.gitignore000064400000000036144761607220006544 0ustar00/vendor/ /tests/ composer.lockREADME.md000064400000000010144761607220006023 0ustar00# dhttp composer.json000064400000001470144761607220007301 0ustar00{ "name": "boru/dhttp", "type": "library", "autoload": { "psr-4": { "boru\\dhttp\\": "src/" } }, "authors": [ { "name": "Daniel Hayes", "email": "dhayes@boruapps.com" } ], "require": { "boru/dhutils": "*", "clue/mq-react": "^1.5", "react/http": "^1.8", "react/async": "^2.0", "react/child-process": "^0.6.5", "fig/http-message-util": "^1.0" }, "suggest": { "boru/dhdb": "dhDB library for database", "boru/dhcli": "CLI toolkit/library for making CLI apps", "boru/dhapi": "Toolkit/library for creating API apps" }, "repositories": [ { "type": "composer", "url": "https://satis.boruapps.com" } ] } examples/init.php000064400000000246144761607220010051 0ustar00method("post"); $request->url("https://devl06.borugroup.com/testclient.php"); $request->form("sleepTime",12); $request->form("identifier","test"); $request->async(true); $promise = $request->send()->then(function($response) { //optionally do something with the response asynchronously. dhGlobal::outLine("Response asyncronously:",$response->asString()); }); dhGlobal::outLine("Sent async request.. now waiting for promise to resolve"); for($i=0;$i<=5;$i++) { $req = new Request(); $req->method("post"); $req->url("http://localhost/testclient.php"); $req->form("min",0.1); $req->form("max",8); $req->form("identifier","test$i"); $req->async(true); $req->send()->then(function($response) use ($i) { dhGlobal::outLine("recieved response $i asyncronously"); }); } //Wait for the promise to resolve before proceeding $request->await(); dhGlobal::outLine("Response after waiting:",$request->response()->asString());examples/requestOptions.php000064400000003236144761607220012154 0ustar00method("post"); $options->url("https://devl06.borugroup.com/testclient.php"); $options->query([ "q"=>"test", ]); $request = new Request($options); $response = $request->send(); dhGlobal::outLine("Response:",$response->asString()); //Send an asynchronous request $options = new Options(); $options->method("post"); $options->url("https://devl06.borugroup.com/testclient.php"); $options->form([ "min"=>2, "max"=>6, "identifier"=>"test", ]); $options->async(true); $request = new Request($options); $promise = $request->send()->then(function($response) { dhGlobal::outLine("Response asyncronously:",$response->asString()); }); dhGlobal::outLine("Sent async request.. now waiting for promise to resolve"); //Generating a couple of standard async requests to test concurrency for($i=0;$i<10;$i++) { $opt = clone $options; $opt->form([ "min"=>0.1, "max"=>2, "identifier"=>"test$i" ]); $request = new Request($opt); $promise = $request->send()->then(function($response) use ($i) { dhGlobal::outLine("recieved response $i asyncronously"); }); } $request->await(); dhGlobal::outLine("Response after waiting:",$request->response()->asString()); examples/staticMethods.php000064400000002175144761607220011724 0ustar00[ "q"=>"test", ] ]); //The shortest and easiest way to send an asynchronous request $promise1 = Client::get("https://devl06.borugroup.com/testclient.php",[],true)->then(function($response) { dhGlobal::outLine("Response:",$response->asString()); }); //Optionally wait for the promise to resolve Client::awaitAll(); //The shortest and easiest way to send an asynchronous request with a body $promise2 = Client::post("https://devl06.borugroup.com/testclient.php",[ "form"=>[ "q"=>"test", ] ],true)->then(function($response) { dhGlobal::outLine("Response:",$response->asString()); });instructions-composer.txt000064400000000300144761607220011700 0ustar00{ "require": { "boru/dhttp": "*" }, "repositories": [ { "type": "composer", "url": "https://satis.boruapps.com" } ] }src/Client.php000064400000043221144761607220007275 0ustar00["User-Agent" => "boru/dhttp",]]; /** @var int */ private static $maxConcurrency=3; /** @var \React\Promise\Deferred[] */ private static $promises=[]; /** @var \boru\dhutils\http\Response[]|\React\Promise\PromiseInterface[]|Exception[] */ private static $results=[]; public static function init($force=false) { if(is_null(static::$browserClient) || $force) { static::$Connector = new \React\Socket\Connector([ "tls"=>[ 'verify_peer' => static::$verifyPeer, 'verify_peer_name' => static::$verifyPeerName, ] ]); static::$browserClient = new \React\Http\Browser(static::$Connector); static::$browserClient = static::$browserClient->withRejectErrorResponse(false); } } public static function setOptions($options=[]) { static::$options = $options; } public static function verifyPeer($verifyPeer=true,$verifyPeerName=true) { static::$verifyPeer = $verifyPeer; static::$verifyPeerName = $verifyPeerName; } /** * * @param MiddlewareInterface $middleware * @return void */ public static function addRequestMiddleware($middleware) { if(is_array($middleware)) { foreach($middleware as $m) { static::addRequestMiddleware($m); } return; } if(is_callable($middleware)) { $middleware = new CallbackMiddleware($middleware); } if(!($middleware instanceof MiddlewareInterface)) { throw new Exception("Invalid middleware"); } static::$options["requestMiddleware"][] = $middleware; } public static function addResponseMiddleware($middleware) { if(is_array($middleware)) { foreach($middleware as $m) { static::addResponseMiddleware($m); } return; } if(is_callable($middleware)) { $middleware = new CallbackMiddleware($middleware); } if(!($middleware instanceof MiddlewareInterface)) { throw new Exception("Invalid middleware"); } static::$options["responseMiddleware"][] = $middleware; } public static function setRequestMiddleware($middleware=[]) { if(empty($middleware)) { static::$options["requestMiddleware"] = []; return; } static::$options["requestMiddleware"] = []; static::addRequestMiddleware($middleware); } public static function setResponseMiddleware($middleware=[]) { if(empty($middleware)) { static::$options["responseMiddleware"] = []; return; } static::$options["responseMiddleware"] = []; static::addResponseMiddleware($middleware); } public static function maxConcurrency($maxConcurrency) { static::$maxConcurrency = $maxConcurrency; } private static function registerDeferred($id,$deferred) { static::$promises[$id] = $deferred; } private static function registerPromise($id,$promise) { static::$results[$id] = $promise; } /** * @param Request $request * @return \React\Promise\PromiseInterface */ public static function send($request) { static::init(); static::registerDeferred($request->getId(),$request->getDeferred()); static::applyMiddleware($request,$request->getRequestMiddleware()); $promise = static::$browserClient->request($request->getMethod(),$request->getFullUrl(),$request->getHeaders(),$request->getBody()); static::registerPromise($request->getId(),$promise); $promise->then(function(\Psr\Http\Message\ResponseInterface $response) use ($request) { static::processSuccess($request,$response); },function(Exception $e) use ($request) { static::processException($request,$e); }); static::checkConcurrency(); return $promise; } private static function processSuccess($request,$response) { if(static::$debugInfo) { dhGlobal::info("dhBrowser response from",$request->getMethod(),$request->getUrl()); } $resp = new Response($response,$request); static::processResponse($request,$resp); return $resp; } private static function processException($request,$e) { if(static::$debugError) { dhGlobal::error("dhBrowser error from",$request->getMethod(),$request->getUrl(),"-",$e->getMessage()); } if($request->getThrowException() && $e instanceof \Exception) { static::processResponse($request,$e); throw $e; } if($e instanceof ResponseException) { $resp = new Response($e->getResponse(),$request); return $resp; } static::processResponse($request,$e); return $e; } /** * @param Request $request * @param mixed $response Response * @return Response * @throws ResponseException * @throws \Exception */ private static function processResponse($request,$response) { if(!isset(static::$promises[$request->getId()])) { return $response; } $deferred = static::$promises[$request->getId()]; unset(static::$promises[$request->getId()]); //throw if we're supposed to, before middleware if($response instanceof \Exception) { static::handleResponseException($request,$response); } if(!($response instanceof Response)) { if($response instanceof ResponseException || method_exists($response,"getResponse")) { $response = new Response($response->getResponse(),$request); } elseif($response instanceof \Exception) { $response = Response::fromException($response,$request); } } try { static::applyMiddleware($response,$request->getResponseMiddleware()); } catch(\Exception $e) { //throw if we're supposed to, after middleware static::handleResponseException($request,$response); } //throw if we're supposed to, after middleware -- final check if($response instanceof \Exception) { static::handleResponseException($request,$response); } static::assignResponse($request->getId(),$response); if($response instanceof \Exception) { $deferred->reject($response); } else { $deferred->resolve($response); } return $response; } private static function handleResponseException($request,$e) { if($request->getThrowException()) { static::assignResponse($request->getId(),$e); throw $e; } return $e; } private static function assignResponse($requestId,$response) { static::$results[$requestId] = $response; } private static function applyMiddleware(&$object,$middlewareList=[]) { if(empty($middlewareList)) { return; } $queue = new \SplQueue(); reset($middlewareList); foreach($middlewareList as $middleware) { $queue->enqueue($middleware); } $next = function($obj) use ($queue,&$next) { if($queue->isEmpty()) { return $obj; } $middleware = $queue->dequeue(); return $middleware($obj,$next); }; $object = $next($object); } /** * @return Response|PromiseInterface|Exception */ public static function getResponse($id) { return isset(static::$results[$id]) ? static::$results[$id] : false; } private static function checkConcurrency() { if(!static::isOverConcurrency()) { return; } Loop::addPeriodicTimer(0.1,function($timer) { if(!static::isOverConcurrency()) { Loop::cancelTimer($timer); Loop::stop(); } }); Loop::run(); } private static function isOverConcurrency() { if(count(static::$promises) < static::$maxConcurrency) { return false; } return true; } public static function awaitAll() { if(!empty(static::$promises)) { Loop::addPeriodicTimer(0.1,function($timer) { if(empty(static::$promises)) { Loop::cancelTimer($timer); Loop::stop(); } }); Loop::run(); } } /** * Create an Options object for use with the Request objects * * See the Options class for more information and all available parameters * * Short list: * * * form - array of form parameters * * * json - array of json parameters * * * headers - array of headers * * * query - array of query parameters * * * raw - string of body content * * * async - bool, whether to return a promise or not * * * throwException - bool, whether to throw an exception on error or not * @param array $options $options An array of guzzle-style options (async=>false, form=>[], json=>[], headers=>[], etc..) * @return Options */ public static function options($options=[]) { return new Options($options); } /** * A shortcut to create a form Options object * @param array $data An array of form data * @param Options $options (optional) An existing Options object to add the form data to */ public static function form($data=[],$options=null) { if($options === null) { $options = static::options(); } $options->form($data); return $options; } /** * A shortcut to create a json Options object * @param array $data An array of json data * @param Options $options (optional) An existing Options object to add the json data to */ public static function json($data=[],$options=null) { if($options === null) { $options = static::options(); } $options->json($data); return $options; } /** * Creates an authenticator middleware for use with addRequestMiddleware on a Request object or on the global Client object * @param mixed $header * @param mixed $token * @return MiddlewareInterface */ public static function authToken($header,$token) { return function($request,$next) use ($header,$token) { $request->header($header,$token); return $next($request); }; } /** * Creates a bearer authenticator middleware for use with addRequestMiddleware on a Request object or on the global Client object * @param mixed $token * @return MiddlewareInterface */ public static function authBearer($token) { return function($request,$next) use ($token) { $request->header("Authorization","Bearer ".$token); return $next($request); }; } /** * Creates a basic authenticator middleware for use with addRequestMiddleware on a Request object or on the global Client object * @param mixed $username * @param mixed $password * @return MiddlewareInterface */ public static function authBasic($username,$password) { return function($request,$next) use ($username,$password) { dhGlobal::outLine("Adding header api-test"); $request->header("Authorization","Basic: ".base64_encode($username.":".$password)); return $next($request); }; } /** * Creates an HMAC authenticator middleware for use with addRequestMiddleware on a Request object or on the global Client object * @param mixed $apiKey * @param mixed $apiSecret * @param array $options * @return HmacAuthentication */ public static function hmacAuthenticator($apiKey,$apiSecret,$options=[ "hashAlgo"=>"sha1", "headerApiKey"=>"x-api-key", "headerApiNonce"=>"x-api-nonce", "headerApiSign"=>"x-api-sign", "signatureTemplate"=>"{APIKEY}:{NONCE}:{PATH}", "withLeadingSlash"=>true, ]) { return new HmacAuthentication($apiKey,$apiSecret,$options); } /** * Send the request and if async=true, return a promise. Otherwise await the return and return the Response * @param mixed $method ["GET","POST","PUT","DELETE","PATCH","HEAD"] * @param mixed $url The URl to request from * @param array $options An array of guzzle-style options (form=>, json=>, headers=>, etc..) * @param bool $async (default: true) if true, return a promise, otherwise await and return the Response object. * @return PromiseInterface|Response|Exception|static */ public static function request($method,$url,$options=[],$async=null) { $staticOpts = new Options(static::$options); $options = $staticOpts->merge($options)->toArray(); if(is_null($async) && isset($options["async"])) { $async = $options["async"]; } else { $async = false; } $options['method'] = $method; $options["url"] = $url; $options["async"] = $async; $request = new Request($options); $result = $request->send(); if($async) { return $result; } $request->await(); return $request->response(); } /** * Send a GET request * @param mixed $url The URl to request from * @param Options|array $options an Options object or An array of guzzle-style options (form=>, json=>, headers=>, etc..) * @param bool $async (default: true) if true, return a promise, otherwise await and return the Response object. * @return PromiseInterface|Response|Exception|static */ public static function get($url,$options=[],$async=null) { return static::request("GET",$url,$options,$async); } /** * Send a POST request * @param mixed $url The URl to request from * @param Options|array $options an Options object or An array of guzzle-style options (form=>, json=>, headers=>, etc..) * @param bool $async (default: true) if true, return a promise, otherwise await and return the Response object. * @return PromiseInterface|Response|Exception|static */ public static function post($url,$options=[],$async=null) { return static::request("POST",$url,$options,$async); } /** * Send a PUT request * @param mixed $url The URl to request from * @param Options|array $options an Options object or An array of guzzle-style options (form=>, json=>, headers=>, etc..) * @param bool $async (default: true) if true, return a promise, otherwise await and return the Response object. * @return PromiseInterface|Response|Exception|static */ public static function put($url,$options=[],$async=null) { return static::request("PUT",$url,$options,$async); } /** * Send a DELETE request * @param mixed $url The URl to request from * @param Options|array $options an Options object or An array of guzzle-style options (form=>, json=>, headers=>, etc..) * @param bool $async (default: true) if true, return a promise, otherwise await and return the Response object. * @return PromiseInterface|Response|Exception|static */ public static function delete($url,$options=[],$async=null) { return static::request("DELETE",$url,$options,$async); } /** * Send a PATCH request * @param mixed $url The URl to request from * @param Options|array $options an Options object or An array of guzzle-style options (form=>, json=>, headers=>, etc..) * @param bool $async (default: true) if true, return a promise, otherwise await and return the Response object. * @return PromiseInterface|Response|Exception|static */ public static function patch($url,$options=[],$async=null) { return static::request("PATCH",$url,$options,$async); } /** * Send a HEAD request * @param mixed $url The URl to request from * @param Options|array $options an Options object or An array of guzzle-style options (form=>, json=>, headers=>, etc..) * @param bool $async (default: true) if true, return a promise, otherwise await and return the Response object. * @return PromiseInterface|Response|Exception|static */ public static function head($url,$options=[],$async=null) { return static::request("HEAD",$url,$options,$async); } }src/core/Options.php000064400000035035144761607220010446 0ustar00setOptions($options); } } public function toArray() { $return = []; if(!empty($this->async)) { $return["async"] = $this->async; } if(!empty($this->headers)) { $return["headers"] = $this->headers; } if(!empty($this->method)) { $return["method"] = $this->method; } if(!empty($this->baseUrl)) { $return["baseUrl"] = $this->baseUrl; } if(!empty($this->url)) { $return["url"] = $this->url; } if(!empty($this->body)) { $return["body"] = $this->body; } if(!empty($this->bodyType)) { $return["bodyType"] = $this->bodyType; } if(!empty($this->query)) { $return["query"] = $this->query; } if(!empty($this->throwExceptions)) { $return["throwExceptions"] = $this->throwExceptions; } if(!empty($this->requestMiddleware)) { $return["requestMiddleware"] = $this->requestMiddleware; } if(!empty($this->responseMiddleware)) { $return["responseMiddleware"] = $this->responseMiddleware; } return $return; } /** * Set options from an array or Options object * @param array|Options $options * @return $this */ public function setOptions($options=[]) { if($options instanceof Options) { $options = $options->toArray(); } $this->optionsToValues($options); return $this; } public function options($options=[]) { return $this->setOptions($options); } public function merge($options=[]) { if($options instanceof Options) { $options = $options->toArray(); } $this->optionsToValues($options); return $this; } public function setMethod($method) { $this->method = strtoupper($method); return $this; } public function method($method) { return $this->setMethod($method); } public function setUrl($url) { $this->url = $url; return $this; } public function url($url) { return $this->setUrl($url); } public function setBaseUrl($url) { $this->baseUrl = $url; return $this; } public function baseUrl($url) { return $this->setBaseUrl($url); } public function query($keyPath,$value=null,$separator=".") { if(is_array($keyPath)) { $this->query = $keyPath; return $this; } dhGlobal::dotAssign($this->query,$keyPath,$value,$separator); return $this; } public function setHeaders($headers=[]) { $this->headers = $headers; return $this; } /** * Set a header or many headers * @param array|string $key * @param mixed $value * @return $this */ public function headers($key,$value=null) { return $this->header($key,$value); } /** * Set a header or many headers * @param array|string $key * @param mixed $value * @return $this */ public function header($key,$value=null) { if(is_array($key)) { foreach($key as $k=>$v) { $this->header($k,$v); } } $this->headers[$key]=$value; return $this; } /** * @param mixed $type One of "form","json","raw" * @return $this */ public function bodyType($type) { $this->bodyType = $type; return $this; } /** * @param mixed $type One of "form","json","raw" * @return $this */ public function bodyAs($type) { return $this->bodyType($type); } /** * Set the body of the request. If $keyPath is an array (or json), it will be used as the body. * @param array|string * @return $this */ public function body($keyPath,$value=null,$separator=".") { if(is_null($value)) { if(!is_array($keyPath)) { $check = json_decode($keyPath,true); if(is_array($check)) { $keyPath = $check; } } if(is_array($keyPath)) { foreach($keyPath as $k=>$v) { $this->body($k,$v,$separator); } } else { $this->body = $keyPath; } } else { dhGlobal::dotAssign($this->body,$keyPath,$value,$separator); } return $this; } /** * Set the body of the request. If $keyPath is an array (or json), it will be used as the body. * * Tries to auto-set the Content-Type header based on the input type. * * If $key is a json string, it will be parsed and used as the body, and the Content-Type header will be set to application/json * * If $key is an array, it will be used as the body, and the Content-Type header will be set to application/x-www-form-urlencoded * * If $key is a string, it will be used as the body, and the Content-Type header will not be modified * @param array|string * @return $this */ public function setBody($key=null,$value=null,$separator=".") { if(is_array($key)) { $this->form($key); return $this; } elseif(is_string($key)) { $check = json_decode($key,true); if(is_array($check)) { $this->json($check); return $this; } else { $this->body = $key; return $this; } } return $this->body($key,$value,$separator); } /** * Set the body of the request as json and set the Content-Type header to application/json * @param array|string * @return $this */ public function json($key=null,$value=null,$separator=".") { $this->header('Content-Type','application/json'); $this->bodyType = "json"; $this->body($key,$value,$separator); return $this; } /** * Set the body of the request as form data and set the Content-Type header to application/x-www-form-urlencoded * @param array|string * @return $this */ public function form($key=null,$value=null,$separator=".") { $this->header('Content-Type','application/x-www-form-urlencoded'); $this->bodyType = "form"; $this->body($key,$value,$separator); return $this; } public function raw($data) { $this->bodyType = "raw"; $this->body = $data; return $this; } /** * Whether to send the request asynchronously. If true, a promise will be returned instead of the response. * @param bool $async (default is false) * @return $this */ public function async($async=true) { $this->async = $async; return $this; } /** * Whether to throw exceptions on error. If false, the response will be returned even if it is an error, and Exceptions will be converted to Response objects. * @param bool $throw (default is true); * @return $this */ public function throwExceptions($throw=true) { $this->throwExceptions = $throw; return $this; } public function authBasic($userPass=[]) { $this->headers["Authorization"] = "Basic ".base64_encode(implode(":",$userPass)); return $this; } public function authToken($token) { $this->headers["Authorization"] = $token; return $this; } /** * Add a middleware to the Request object. Middleware will be called in the order they are added and will be passed the request object as the first parameter and the next middleware as the second parameter. * @param MiddlewareInterface|Callable $middleware * @return $this */ public function addRequestMiddleware($middleware) { if(is_array($middleware)) { foreach($middleware as $m) { $this->addRequestMiddleware($m); } return; } if(is_callable($middleware)) { $middleware = new CallbackMiddleware($middleware); } if(!($middleware instanceof MiddlewareInterface)) { throw new Exception("Invalid middleware"); } $this->requestMiddleware[] = $middleware; } /** * Add a middleware to the response. Middleware will be called in the order they are added and will be passed the response or exception object as the first parameter and the next middleware as the second parameter. * @param MiddlewareInterface|Callable $middleware * @return $this */ public function addResponseMiddleware($middleware) { if(is_array($middleware)) { foreach($middleware as $m) { $this->addResponseMiddleware($m); } return; } if(is_callable($middleware)) { $middleware = new CallbackMiddleware($middleware); } if(!($middleware instanceof MiddlewareInterface)) { throw new Exception("Invalid middleware"); } $this->responseMiddleware[] = $middleware; } public function setRequestMiddleware($middleware=[]) { if(empty($middleware)) { $this->requestMiddleware = []; return; } $this->requestMiddleware = []; $this->addRequestMiddleware($middleware); } public function setResponseMiddleware($middleware=[]) { if(empty($middleware)) { $this->responseMiddleware = []; return; } $this->responseMiddleware = []; $this->addResponseMiddleware($middleware); } private function optionsToValues($options=[]) { foreach($options as $think=>$value) { switch($think) { case "body": $this->body($value); break; case "json": $this->json($value); break; case "form": $this->form($value); break; case "raw": $this->raw($value); break; case "query": $this->query($value); break; case "headers": foreach($value as $k=>$v) { $this->header($k,$v); } break; case "auth": if(is_array($value)) { $this->authBasic($value); } else { $this->authToken($value); } break; case "bodyType": $this->bodyType($value); break; case "as": $this->bodyType($value); break; case "method": $this->setMethod($value); break; case "url": $this->setUrl($value); break; case "baseUrl": $this->setBaseUrl($value); break; case "async": $this->async($value); break; case "requestMiddleware": $this->setRequestMiddleware($value); break; case "responseMiddleware": $this->setResponseMiddleware($value); break; case "throwExceptions": $this->throwExceptions($value); break; } } } public function getMethod() { return $this->method; } public function getUrl() { return $this->url; } public function getUrlParts() { return parse_url($this->getFullUrl()); } public function getFullUrl() { $url = $this->url; if(strtolower(substr($url,0,4)) != "http") { $preurl = $this->baseUrl; if(substr($preurl,-1) != "/" && substr($url,0,1) != "/") { $preurl .= "/"; } $url = $preurl.$url; } if(!empty($this->query)) { if(strpos($url,"?")===false) { $url .= "?"; } else { $url .= "&"; } $url .= http_build_query($this->query); } return $url; } public function getHeaders() { return !is_array($this->headers) ? [] : $this->headers; } public function getRawBody() { return $this->body; } public function getBody() { $body = $this->body; if(is_array($body)) { if($this->bodyType == "form") { $body = http_build_query($body); } else if($this->bodyType == "json") { $body = json_encode($body); } } return $body; } public function getBodyType() { return $this->bodyType; } public function getQuery() { return $this->query; } public function getQueryAsString() { return http_build_query($this->query); } public function getResponseMiddleware() { return $this->responseMiddleware; } public function getRequestMiddleware() { return $this->requestMiddleware; } public function getAsync() { return $this->async; } public function isAsync() { return $this->async; } public function getThrowExceptions() { return $this->throwExceptions; } }src/core/Request.php000064400000015370144761607220010443 0ustar00id = uniqid(); if($options instanceof Options) { $this->options = $options; } else if(is_array($options)) { $this->options = new Options($options); } else { throw new Exception("Invalid options type"); } $this->deferred = new Deferred(); } public function __destruct() { if($this->isAsync() && !$this->isDone()) { $this->await(); } } public function setOptions($options=[]) { $this->options->setOptions($options); return $this; } public function setMethod($method) { $this->options->setMethod($method); return $this; } public function setUrl($url) { $this->options->setUrl($url); return $this; } public function setBaseUrl($url) { $this->options->setBaseUrl($url); return $this; } public function query($keyPath,$value=null,$separator=".") { $this->options->query($keyPath,$value,$separator); return $this; } public function method($method) { $this->options->setMethod($method); return $this; } public function url($url) { $this->options->setUrl($url); return $this; } public function header($key,$value=null) { $this->options->header($key,$value); return $this; } /** * @param mixed $type One of "form","json","raw" * @return $this */ public function bodyType($type) { $this->options->bodyType($type); return $this; } public function bodyAs($type) { return $this->bodyType($type); } public function body($keyPath,$value=null,$separator=".") { $this->options->body($keyPath,$value,$separator); return $this; } public function setBody($key=null,$value=null,$separator=".") { $this->options->setBody($key,$value,$separator); return $this; } public function json($key=null,$value=null,$separator=".") { $this->options->json($key,$value,$separator); return $this; } public function form($key=null,$value=null,$separator=".") { $this->options->form($key,$value,$separator); return $this; } public function raw($data) { $this->options->raw($data); return $this; } public function async($async=true) { $this->options->async($async); return $this; } public function authBasic($userPass=[]) { $this->options->authBasic($userPass); return $this; } public function authToken($token) { $this->options->authToken($token); return $this; } /** * * @param MiddlewareInterface $middleware * @return void */ public function addRequestMiddleware($middleware) { $this->options->addRequestMiddleware($middleware); } public function addResponseMiddleware($middleware) { $this->options->addResponseMiddleware($middleware); } public function setRequestMiddleware($middleware=[]) { $this->options->setRequestMiddleWare($middleware); } public function setResponseMiddleware($middleware=[]) { $this->options->setResponseMiddleWare($middleware); } public function getMethod() { return $this->options->getMethod(); } public function getUrl() { return $this->options->getUrl(); } public function getFullUrl() { return $this->options->getFullUrl(); } public function getHeaders() { return $this->options->getHeaders(); } public function getRawBody() { return $this->options->getRawBody(); } public function getBody() { $body = $this->options->getBody(); if(empty($body)) { return ''; } return $body; } public function getBodyType() { return $this->options->getBodyType(); } public function getQuery() { return $this->options->getQuery(); } public function getQueryAsString() { return http_build_query($this->getQuery()); } public function getResponseMiddleware() { return $this->options->getResponseMiddleware(); } public function getRequestMiddleware() { return $this->options->getRequestMiddleware(); } public function getDeferred() { return $this->deferred; } public function getPromise() { return $this->promise; } public function isAsync() { return $this->options->isAsync(); } public function getThrowExceptions() { return $this->options->getThrowExceptions(); } public function getId() { return $this->id; } public function send($options=[]) { $this->options->setOptions($options); $this->promise = Client::send($this); if($this->isAsync()) { return $this->deferred->promise(); } $this->await(); return $this->response(); } public function isDone() { return $this->response() !== false; } /** * Get the response * @return Response|\Exception|false */ public function response() { if(is_null($this->response)) { $response = Client::getResponse($this->id); if($response instanceof PromiseInterface) { return false; } $this->response = $response; } return $this->response; } /** * Await the response and return it * @return bool */ public function await() { if($this->isDone()) { return $this->response(); } Loop::addPeriodicTimer(0.1,function($timer) { if($this->isDone()) { Loop::cancelTimer($timer); Loop::stop(); } }); Loop::run(); return true; } }src/core/Response.php000064400000010076144761607220010607 0ustar00response = $response; $this->request = $request; } public function code() { return $this->getStatusCode(); } public function phrase() { return $this->getReasonPhrase(); } public function header($header=null,$default=false) { if(is_null($header)) { return $this->getHeaders(); } if(!$this->hasHeader($header)) { return $default; } return $this->getHeader($header); } public function headerLine($header,$default=false) { if(!$this->hasHeader($header)) { return $default; } else { return $this->getHeaderLine($header); } } public function request() { return $this->request; } /** * * @param bool $json * @param bool $arr * @return mixed */ public function body($json=false,$arr=true) { if($json) { return json_decode($this->asString(),$arr); } else { return $this->asString(); } } public function asString() { if(is_null($this->bodyString)) { $this->bodyString = (string) $this->getBody()->__toString(); } return $this->bodyString; } public function asArray() { return $this->body(true); } public function asObject() { return $this->body(true,false); } //ResponseInterface methods: public function getStatusCode() { return $this->response->getStatusCode(); } public function withStatus($code, $reasonPhrase = '') { return $this->response->getStatusCode($code, $reasonPhrase); } public function getReasonPhrase() { return $this->response->getReasonPhrase(); } public function getProtocolVersion() { return $this->response->getProtocolVersion(); } public function withProtocolVersion($version) { return $this->response->withProtocolVersion($version); } public function getHeaderLine($name) { return $this->response->getHeaderLine($name); } public function withHeader($name, $value) { return $this->response->withHeader($name, $value); } public function withAddedHeader($name, $value) { return $this->response->withAddedHeader($name, $value); } public function withoutHeader($name) { return $this->response->withoutHeader($name); } public function hasHeader($header) { return $this->response->hasHeader($header); } public function getHeader($header) { return $this->response->getHeader($header); } public function getHeaders() { return $this->response->getHeaders(); } public function getBody() { return $this->response->getBody(); } public function withBody(\Psr\Http\Message\StreamInterface $body) { return $this->response->withBody($body); } public static function create($code,$phrase,$headers,$body) { $response = new \React\Http\Message\Response($code,$headers,$body,"1.1",$phrase); $response = $response->withStatus($code,$phrase); foreach($headers as $header=>$value) { $response = $response->withHeader($header,$value); } return new Response($response,null); } public static function fromException($exception) { $code = 500; $phrase = "Error ".$exception->getCode()." - ".$exception->getMessage(); $headers = []; $body = $exception->getTraceAsString(); return self::create($code,$phrase,$headers,$body); } }src/middleware/CallbackMiddleware.php000064400000000540144761607220013663 0ustar00callback = $callback; } public function __invoke($object,$next) { $callback = $this->callback; return $callback($object,$next); } }src/middleware/HmacAuthentication.php000064400000006176144761607220013754 0ustar00header($this->headerApiKey,$this->apiKey); $object->header($this->headerApiNonce,$this->getNonce()); $object->header($this->headerApiSign,$this->getSignature($object)); return $next($object); } public function __construct($apiKey,$apiSecret,$options=[]) { $this->apiKey = $apiKey; $this->apiSecret = $apiSecret; if(isset($options["headerApiKey"])) { $this->headerApiKey = $options["headerApiKey"]; } if(isset($options["headerApiNonce"])) { $this->headerApiNonce = $options["headerApiNonce"]; } if(isset($options["headerApiSign"])) { $this->headerApiSign = $options["headerApiSign"]; } if(isset($options["signatureTemplate"])) { $this->signatureTemplate = $options["signatureTemplate"]; } if(isset($options["hashAlgo"])) { $this->hashAlgo = $options["hashAlgo"]; } if(isset($options["withLeadingSlash"])) { $this->withLeadingSlash = $options["withLeadingSlash"]; } } public function setApiKey($apiKey) { $this->apiKey = $apiKey; } public function setApiSecret($apiSecret) { $this->apiSecret = $apiSecret; } public function setHeaderApiKey($headerApiKey) { $this->headerApiKey = $headerApiKey; } public function setHeaderApiNonce($headerApiNonce) { $this->headerApiNonce = $headerApiNonce; } public function setHeaderApiSign($headerApiSign) { $this->headerApiSign = $headerApiSign; } public function setSignatureTemplate($signatureTemplate) { $this->signatureTemplate = $signatureTemplate; } public function setHashAlgo($hashAlgo) { $this->hashAlgo = $hashAlgo; } public function getNonce() { return time(); } public function getSignature($object) { $path = $this->getPath($object); $nonce = $this->getNonce(); $signature = str_replace("{APIKEY}",$this->apiKey,$this->signatureTemplate); $signature = str_replace("{NONCE}",$nonce,$signature); $signature = str_replace("{PATH}",$path,$signature); $signature = hash_hmac($this->hashAlgo,$signature,$this->apiSecret); return $signature; } private function getPath($object) { $fullUrl = $object->getFullUrl(); $path = dhGlobal::trimString(parse_url($fullUrl, PHP_URL_PATH),"/"); if($this->withLeadingSlash) { $path = "/".$path; } return $path; } }src/middleware/MiddlewareInterface.php000064400000000173144761607220014071 0ustar00hwm = $hwm; } public function __toString() { return $this->getContents(); } public function getContents() { $buffer = $this->buffer; $this->buffer = ''; return $buffer; } public function close() { $this->buffer = ''; } public function detach() { $this->close(); return null; } public function getSize() { return strlen($this->buffer); } public function isReadable() { return true; } public function isWritable() { return true; } public function isSeekable() { return false; } public function rewind() { $this->seek(0); } public function seek($offset, $whence = SEEK_SET) { throw new \RuntimeException('Cannot seek a BufferStream'); } public function eof() { return strlen($this->buffer) === 0; } public function tell() { throw new \RuntimeException('Cannot determine the position of a BufferStream'); } /** * Reads data from the buffer. */ public function read($length) { $currentLength = strlen($this->buffer); if ($length >= $currentLength) { // No need to slice the buffer because we don't have enough data. $result = $this->buffer; $this->buffer = ''; } else { // Slice up the result to provide a subset of the buffer. $result = substr($this->buffer, 0, $length); $this->buffer = substr($this->buffer, $length); } return $result; } /** * Writes data to the buffer. */ public function write($string) { $this->buffer .= $string; // TODO: What should happen here? if (strlen($this->buffer) >= $this->hwm) { return false; } return strlen($string); } public function getMetadata($key = null) { if ($key == 'hwm') { return $this->hwm; } return $key ? null : []; } } src/psr7/PumpStream.php000064400000010237144761607220011050 0ustar00source = $source; $this->size = isset($options['size']) ? $options['size'] : null; $this->metadata = isset($options['metadata']) ? $options['metadata'] : []; $this->buffer = new BufferStream(); } public function __toString() { try { return Stream::copyToString($this); } catch (\Exception $e) { return ''; } } public function close() { $this->detach(); } public function detach() { $this->tellPos = false; $this->source = null; return null; } public function getSize() { return $this->size; } public function tell() { return $this->tellPos; } public function eof() { return !$this->source; } public function isSeekable() { return false; } public function rewind() { $this->seek(0); } public function seek($offset, $whence = SEEK_SET) { throw new \RuntimeException('Cannot seek a PumpStream'); } public function isWritable() { return false; } public function write($string) { throw new \RuntimeException('Cannot write to a PumpStream'); } public function isReadable() { return true; } public function read($length) { $data = $this->buffer->read($length); $readLen = strlen($data); $this->tellPos += $readLen; $remaining = $length - $readLen; if ($remaining) { $this->pump($remaining); $data .= $this->buffer->read($remaining); $this->tellPos += strlen($data) - $readLen; } return $data; } public function getContents() { $result = ''; while (!$this->eof()) { $result .= $this->read(1000000); } return $result; } public function getMetadata($key = null) { if (!$key) { return $this->metadata; } return isset($this->metadata[$key]) ? $this->metadata[$key] : null; } private function pump($length) { if ($this->source) { do { $data = call_user_func($this->source, $length); if ($data === false || $data === null) { $this->source = null; return; } $this->buffer->write($data); $length -= strlen($data); } while ($length > 0); } } } src/psr7/Request.php000064400000007234144761607220010406 0ustar00 * @author Martijn van der Ven * * Copied from Nyholm/Psr7 -- modified for boru\dhttp; */ class Request implements \Psr\Http\Message\RequestInterface { use MessageTrait; /** @var string|null */ private $requestTarget; /** @var string */ private $method; /** @var UriInterface|null */ private $uri; /** * @param string $method HTTP method * @param string|UriInterface $uri URI * @param array $headers Request headers * @param string|resource|StreamInterface|null $body Request body * @param string $version Protocol version */ public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1') { if (!($uri instanceof UriInterface)) { $uri = new Uri($uri); } $this->method = $method; $this->uri = $uri; $this->setHeaders($headers); $this->protocol = $version; if (!$this->hasHeader('Host')) { $this->updateHostFromUri(); } // If we got no body, defer initialization of the stream until Request::getBody() if ('' !== $body && null !== $body) { $this->stream = Stream::create($body); } } public function getRequestTarget() { if ($this->requestTarget !== null) { return $this->requestTarget; } $target = $this->uri->getPath(); if ($target === '') { $target = '/'; } if ($this->uri->getQuery() != '') { $target .= '?' . $this->uri->getQuery(); } return $target; } public function withRequestTarget(string $requestTarget) { if (preg_match('#\s#', $requestTarget)) { throw new \InvalidArgumentException( 'Invalid request target provided; cannot contain whitespace' ); } $new = clone $this; $new->requestTarget = $requestTarget; return $new; } public function getMethod() { return $this->method; } public function withMethod(string $method) { $this->assertMethod($method); $new = clone $this; $new->method = strtoupper($method); return $new; } public function getUri() { return $this->uri; } public function withUri(UriInterface $uri, bool $preserveHost = false) { if ($uri === $this->uri) { return $this; } $new = clone $this; $new->uri = $uri; if (!$preserveHost || !isset($this->headerNames['host'])) { $new->updateHostFromUri(); } return $new; } private function updateHostFromUri() { $host = $this->uri->getHost(); if ($host == '') { return; } if (($port = $this->uri->getPort()) !== null) { $host .= ':' . $port; } if (isset($this->headerNames['host'])) { $header = $this->headerNames['host']; } else { $header = 'Host'; $this->headerNames['host'] = 'Host'; } // Ensure Host is the first header. // See: http://tools.ietf.org/html/rfc7230#section-5.4 $this->headers = [$header => [$host]] + $this->headers; } }src/psr7/Stream.php000064400000033467144761607220010220 0ustar00size = $options['size']; } $this->customMetadata = isset($options['metadata']) ? $options['metadata'] : []; $this->stream = $stream; $meta = stream_get_meta_data($this->stream); $this->seekable = $meta['seekable']; $this->readable = (bool)preg_match(self::READABLE_MODES, $meta['mode']); $this->writable = (bool)preg_match(self::WRITABLE_MODES, $meta['mode']); $this->uri = $this->getMetadata('uri'); } /** * Closes the stream when the destructed */ public function __destruct() { $this->close(); } public function __toString() { try { if ($this->isSeekable()) { $this->seek(0); } return $this->getContents(); } catch (\Exception $e) { return ''; } } public function getContents() { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } $contents = stream_get_contents($this->stream); if ($contents === false) { throw new \RuntimeException('Unable to read stream contents'); } return $contents; } public function close() { if (isset($this->stream)) { if (is_resource($this->stream)) { fclose($this->stream); } $this->detach(); } } public function detach() { if (!isset($this->stream)) { return null; } $result = $this->stream; unset($this->stream); $this->size = $this->uri = null; $this->readable = $this->writable = $this->seekable = false; return $result; } public function getSize() { if ($this->size !== null) { return $this->size; } if (!isset($this->stream)) { return null; } // Clear the stat cache if the stream has a URI if ($this->uri) { clearstatcache(true, $this->uri); } $stats = fstat($this->stream); if (isset($stats['size'])) { $this->size = $stats['size']; return $this->size; } return null; } public function isReadable() { return $this->readable; } public function isWritable() { return $this->writable; } public function isSeekable() { return $this->seekable; } public function eof() { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } return feof($this->stream); } public function tell() { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } $result = ftell($this->stream); if ($result === false) { throw new \RuntimeException('Unable to determine stream position'); } return $result; } public function rewind() { $this->seek(0); } public function seek($offset, $whence = SEEK_SET) { $whence = (int) $whence; if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } if (!$this->seekable) { throw new \RuntimeException('Stream is not seekable'); } if (fseek($this->stream, $offset, $whence) === -1) { throw new \RuntimeException('Unable to seek to stream position ' . $offset . ' with whence ' . var_export($whence, true)); } } public function read($length) { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } if (!$this->readable) { throw new \RuntimeException('Cannot read from non-readable stream'); } if ($length < 0) { throw new \RuntimeException('Length parameter cannot be negative'); } if (0 === $length) { return ''; } $string = fread($this->stream, $length); if (false === $string) { throw new \RuntimeException('Unable to read from stream'); } return $string; } public function write($string) { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); } if (!$this->writable) { throw new \RuntimeException('Cannot write to a non-writable stream'); } // We can't know the size after writing anything $this->size = null; $result = fwrite($this->stream, $string); if ($result === false) { throw new \RuntimeException('Unable to write to stream'); } return $result; } public function getMetadata($key = null) { if (!isset($this->stream)) { return $key ? null : []; } elseif (!$key) { return $this->customMetadata + stream_get_meta_data($this->stream); } elseif (isset($this->customMetadata[$key])) { return $this->customMetadata[$key]; } $meta = stream_get_meta_data($this->stream); return isset($meta[$key]) ? $meta[$key] : null; } /** * Create a new stream based on the input type. * * Options is an associative array that can contain the following keys: * - metadata: Array of custom metadata. * - size: Size of the stream. * * This method accepts the following `$resource` types: * - `Psr\Http\Message\StreamInterface`: Returns the value as-is. * - `string`: Creates a stream object that uses the given string as the contents. * - `resource`: Creates a stream object that wraps the given PHP stream resource. * - `Iterator`: If the provided value implements `Iterator`, then a read-only * stream object will be created that wraps the given iterable. Each time the * stream is read from, data from the iterator will fill a buffer and will be * continuously called until the buffer is equal to the requested read size. * Subsequent read calls will first read from the buffer and then call `next` * on the underlying iterator until it is exhausted. * - `object` with `__toString()`: If the object has the `__toString()` method, * the object will be cast to a string and then a stream will be returned that * uses the string value. * - `NULL`: When `null` is passed, an empty stream object is returned. * - `callable` When a callable is passed, a read-only stream object will be * created that invokes the given callable. The callable is invoked with the * number of suggested bytes to read. The callable can return any number of * bytes, but MUST return `false` when there is no more data to return. The * stream object that wraps the callable will invoke the callable until the * number of requested bytes are available. Any additional bytes will be * buffered and used in subsequent reads. * * @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data * @param array $options Additional options * * @return StreamInterface * * @throws \InvalidArgumentException if the $resource arg is not valid. */ public static function create($resource = '', array $options = []) { if (is_scalar($resource)) { $stream = self::tryFopen('php://temp', 'r+'); if ($resource !== '') { fwrite($stream, $resource); fseek($stream, 0); } return new Stream($stream, $options); } switch (gettype($resource)) { case 'resource': /* * The 'php://input' is a special stream with quirks and inconsistencies. * We avoid using that stream by reading it into php://temp */ $metaData = \stream_get_meta_data($resource); if (isset($metaData['uri']) && $metaData['uri'] === 'php://input') { $stream = self::tryFopen('php://temp', 'w+'); fwrite($stream, stream_get_contents($resource)); fseek($stream, 0); $resource = $stream; } return new Stream($resource, $options); case 'object': if ($resource instanceof StreamInterface) { return $resource; } elseif ($resource instanceof \Iterator) { return new PumpStream(function () use ($resource) { if (!$resource->valid()) { return false; } $result = $resource->current(); $resource->next(); return $result; }, $options); } elseif (method_exists($resource, '__toString')) { return static::create((string) $resource, $options); } break; case 'NULL': return new Stream(self::tryFopen('php://temp', 'r+'), $options); } if (is_callable($resource)) { return new PumpStream($resource, $options); } throw new \InvalidArgumentException('Invalid resource type: ' . gettype($resource)); } /** * Safely opens a PHP stream resource using a filename. * * When fopen fails, PHP normally raises a warning. This function adds an * error handler that checks for errors and throws an exception instead. * * @param string $filename File to open * @param string $mode Mode used to open the file * * @return resource * * @throws \RuntimeException if the file cannot be opened */ public static function tryFopen($filename, $mode) { $ex = null; set_error_handler(function () use ($filename, $mode, &$ex) { $ex = new \RuntimeException(sprintf( 'Unable to open "%s" using mode "%s": %s', $filename, $mode, func_get_args()[1] )); return true; }); try { $handle = fopen($filename, $mode); } catch (\Exception $e) { $ex = new \RuntimeException(sprintf( 'Unable to open "%s" using mode "%s": %s', $filename, $mode, $e->getMessage() ), 0, $e); } restore_error_handler(); if ($ex) { /** @var $ex \RuntimeException */ throw $ex; } return $handle; } /** * Copy the contents of a stream into a string until the given number of * bytes have been read. * * @param StreamInterface $stream Stream to read * @param int $maxLen Maximum number of bytes to read. Pass -1 * to read the entire stream. * * @return string * * @throws \RuntimeException on error. */ public static function copyToString(StreamInterface $stream, $maxLen = -1) { $buffer = ''; if ($maxLen === -1) { while (!$stream->eof()) { $buf = $stream->read(1048576); // Using a loose equality here to match on '' and false. if ($buf == null) { break; } $buffer .= $buf; } return $buffer; } $len = 0; while (!$stream->eof() && $len < $maxLen) { $buf = $stream->read($maxLen - $len); // Using a loose equality here to match on '' and false. if ($buf == null) { break; } $buffer .= $buf; $len = strlen($buffer); } return $buffer; } } src/psr7/Uri.php000064400000022347144761607220007517 0ustar00 * @author Martijn van der Ven * * Copied from Nyholm/Psr7 -- modified for boru\dhttp; * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ class Uri implements \Psr\Http\Message\UriInterface { const SCHEMES = ['http' => 80, 'https' => 443]; const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~'; const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;='; const CHAR_GEN_DELIMS = ':\/\?#\[\]@'; /** @var string Uri scheme. */ private $scheme = ''; /** @var string Uri user info. */ private $userInfo = ''; /** @var string Uri host. */ private $host = ''; /** @var int|null Uri port. */ private $port; /** @var string Uri path. */ private $path = ''; /** @var string Uri query string. */ private $query = ''; /** @var string Uri fragment. */ private $fragment = ''; public function __construct(string $uri='') { if ('' !== $uri) { if (false === $parts = \parse_url($uri)) { throw new \InvalidArgumentException(\sprintf('Unable to parse URI: "%s"', $uri)); } // Apply parse_url parts to a URI. $this->scheme = isset($parts['scheme']) ? \strtr($parts['scheme'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; $this->userInfo = isset($parts['user']) ? $parts['user'] : ''; $this->host = isset($parts['host']) ? \strtr($parts['host'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null; $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : ''; $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : ''; $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : ''; if (isset($parts['pass'])) { $this->userInfo .= ':' . $parts['pass']; } } } public function __toString() { return self::createUriString($this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment); } public function getScheme() { return $this->scheme; } public function getAuthority() { if ('' === $this->host) { return ''; } $authority = $this->host; if ('' !== $this->userInfo) { $authority = $this->userInfo . '@' . $authority; } if (null !== $this->port) { $authority .= ':' . $this->port; } return $authority; } public function getUserInfo() { return $this->userInfo; } public function getHost() { return $this->host; } public function getPort() { return $this->port; } public function getPath() { $path = $this->path; if ('' !== $path && '/' !== $path[0]) { if ('' !== $this->host) { // If the path is rootless and an authority is present, the path MUST be prefixed by "/" $path = '/' . $path; } } elseif (isset($path[1]) && '/' === $path[1]) { // If the path is starting with more than one "/", the // starting slashes MUST be reduced to one. $path = '/' . \ltrim($path, '/'); } return $path; } public function getQuery() { return $this->query; } public function getFragment() { return $this->fragment; } public function withScheme($scheme) { if (!\is_string($scheme)) { throw new \InvalidArgumentException('Scheme must be a string'); } if ($this->scheme === $scheme = \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { return $this; } $new = clone $this; $new->scheme = $scheme; $new->port = $new->filterPort($new->port); return $new; } public function withUserInfo($user, $password = null) { if (!\is_string($user)) { throw new \InvalidArgumentException('User must be a string'); } $info = \preg_replace_callback('/[' . self::CHAR_GEN_DELIMS . self::CHAR_SUB_DELIMS . ']++/', [__CLASS__, 'rawurlencodeMatchZero'], $user); if (null !== $password && '' !== $password) { if (!\is_string($password)) { throw new \InvalidArgumentException('Password must be a string'); } $info .= ':' . \preg_replace_callback('/[' . self::CHAR_GEN_DELIMS . self::CHAR_SUB_DELIMS . ']++/', [__CLASS__, 'rawurlencodeMatchZero'], $password); } if ($this->userInfo === $info) { return $this; } $new = clone $this; $new->userInfo = $info; return $new; } public function withHost($host) { if (!\is_string($host)) { throw new \InvalidArgumentException('Host must be a string'); } if ($this->host === $host = \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { return $this; } $new = clone $this; $new->host = $host; return $new; } public function withPort($port) { if ($this->port === $port = $this->filterPort($port)) { return $this; } $new = clone $this; $new->port = $port; return $new; } public function withPath($path) { if ($this->path === $path = $this->filterPath($path)) { return $this; } $new = clone $this; $new->path = $path; return $new; } public function withQuery($query) { if ($this->query === $query = $this->filterQueryAndFragment($query)) { return $this; } $new = clone $this; $new->query = $query; return $new; } public function withFragment($fragment) { if ($this->fragment === $fragment = $this->filterQueryAndFragment($fragment)) { return $this; } $new = clone $this; $new->fragment = $fragment; return $new; } /** * Create a URI string from its various parts. */ private static function createUriString(string $scheme, string $authority, string $path, string $query, string $fragment) { $uri = ''; if ('' !== $scheme) { $uri .= $scheme . ':'; } if ('' !== $authority) { $uri .= '//' . $authority; } if ('' !== $path) { if ('/' !== $path[0]) { if ('' !== $authority) { // If the path is rootless and an authority is present, the path MUST be prefixed by "/" $path = '/' . $path; } } elseif (isset($path[1]) && '/' === $path[1]) { if ('' === $authority) { // If the path is starting with more than one "/" and no authority is present, the // starting slashes MUST be reduced to one. $path = '/' . \ltrim($path, '/'); } } $uri .= $path; } if ('' !== $query) { $uri .= '?' . $query; } if ('' !== $fragment) { $uri .= '#' . $fragment; } return $uri; } /** * Is a given port non-standard for the current scheme? */ private static function isNonStandardPort(string $scheme, int $port) { return !isset(self::SCHEMES[$scheme]) || $port !== self::SCHEMES[$scheme]; } private function filterPort($port) { if (null === $port) { return null; } $port = (int) $port; if (0 > $port || 0xFFFF < $port) { throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 0 and 65535', $port)); } return self::isNonStandardPort($this->scheme, $port) ? $port : null; } private function filterPath($path) { if (!\is_string($path)) { throw new \InvalidArgumentException('Path must be a string'); } return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $path); } private function filterQueryAndFragment($str) { if (!\is_string($str)) { throw new \InvalidArgumentException('Query and fragment must be a string'); } return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $str); } private static function rawurlencodeMatchZero(array $match) { return \rawurlencode($match[0]); } }src/psr7/traits/MessageTrait.php000064400000015130144761607220012646 0ustar00 array of values */ private $headers = []; /** @var array Map of lowercase header name => original name at registration */ private $headerNames = []; /** @var string */ private $protocol = '1.1'; /** @var StreamInterface|null */ private $stream; public function getProtocolVersion() { return $this->protocol; } public function withProtocolVersion(string $version) { if ($this->protocol === $version) { return $this; } $new = clone $this; $new->protocol = $version; return $new; } public function getHeaders() { return $this->headers; } public function hasHeader(string $header) { return isset($this->headerNames[strtolower($header)]); } public function getHeader(string $header) { $header = strtolower($header); if (!isset($this->headerNames[$header])) { return []; } $header = $this->headerNames[$header]; return $this->headers[$header]; } public function getHeaderLine(string $header) { return implode(', ', $this->getHeader($header)); } public function withHeader(string $header, $value) { $this->assertHeader($header); $value = $this->normalizeHeaderValue($value); $normalized = strtolower($header); $new = clone $this; if (isset($new->headerNames[$normalized])) { unset($new->headers[$new->headerNames[$normalized]]); } $new->headerNames[$normalized] = $header; $new->headers[$header] = $value; } public function withAddedHeader(string $header, $value) { $this->assertHeader($header); $value = $this->normalizeHeaderValue($value); $normalized = strtolower($header); $new = clone $this; if (isset($new->headerNames[$normalized])) { $header = $this->headerNames[$normalized]; $new->headers[$header] = array_merge($this->headers[$header], $value); } else { $new->headerNames[$normalized] = $header; $new->headers[$header] = $value; } return $new; } public function withoutHeader(string $header) { $normalized = strtolower($header); if (!isset($this->headerNames[$normalized])) { return $this; } $header = $this->headerNames[$normalized]; $new = clone $this; unset($new->headers[$header], $new->headerNames[$normalized]); return $new; } public function getBody() { if (!$this->stream) { $this->stream = Utils::streamFor(''); } return $this->stream; } public function withBody(StreamInterface $body) { if ($body === $this->stream) { return $this; } $new = clone $this; $new->stream = $body; return $new; } private function setHeaders(array $headers) { $this->headerNames = $this->headers = []; foreach ($headers as $header => $value) { // Numeric array keys are converted to int by PHP. $header = (string) $header; $this->assertHeader($header); $value = $this->normalizeHeaderValue($value); $normalized = strtolower($header); if (isset($this->headerNames[$normalized])) { $header = $this->headerNames[$normalized]; $this->headers[$header] = array_merge($this->headers[$header], $value); } else { $this->headerNames[$normalized] = $header; $this->headers[$header] = $value; } } } private function normalizeHeaderValue($value) { if (!is_array($value)) { return $this->trimAndValidateHeaderValues([$value]); } if (count($value) === 0) { throw new \InvalidArgumentException('Header value can not be an empty array.'); } return $this->trimAndValidateHeaderValues($value); } private function trimAndValidateHeaderValues(array $values) { return array_map(function ($value) { if (!is_scalar($value) && null !== $value) { throw new \InvalidArgumentException(sprintf( 'Header value must be scalar or null but %s provided.', is_object($value) ? get_class($value) : gettype($value) )); } $trimmed = trim((string) $value, " \t"); $this->assertValue($trimmed); return $trimmed; }, array_values($values)); } private function assertHeader($header) { if (!is_string($header)) { throw new \InvalidArgumentException(sprintf( 'Header name must be a string but %s provided.', is_object($header) ? get_class($header) : gettype($header) )); } if (! preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $header)) { throw new \InvalidArgumentException( sprintf('"%s" is not valid header name.', $header) ); } } private function assertValue(string $value) { // The regular expression intentionally does not support the obs-fold production, because as // per RFC 7230#3.2.4: // // A sender MUST NOT generate a message that includes // line folding (i.e., that has any field-value that contains a match to // the obs-fold rule) unless the message is intended for packaging // within the message/http media type. // // Clients must not send a request with line folding and a server sending folded headers is // likely very rare. Line folding is a fairly obscure feature of HTTP/1.1 and thus not accepting // folding is not likely to break any legitimate use case. if (! preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D', $value)) { throw new \InvalidArgumentException( sprintf('"%s" is not valid header value.', $value) ); } } private function assertMethod($method) { if (!is_string($method) || $method === '') { throw new \InvalidArgumentException('Method must be a non-empty string.'); } } }