You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
362 lines
9.9 KiB
PHP
362 lines
9.9 KiB
PHP
<?php
|
|
|
|
namespace Proxy\Http;
|
|
|
|
use Proxy\Http\ParamStore;
|
|
|
|
class Request {
|
|
|
|
private $method;
|
|
private $url;
|
|
|
|
private $protocol_version = '1.1';
|
|
|
|
// Custom attributes to go with each Request instance - has nothing to do with HTTP
|
|
public $params; // parameters
|
|
|
|
// HTTP headers for this request - all in lowercase
|
|
public $headers;
|
|
|
|
// Here we store cookies for that request
|
|
//public $cookies;
|
|
|
|
// Collection of POST fields to be submitted
|
|
public $post;
|
|
|
|
// Query string parameters for URL
|
|
public $get;
|
|
|
|
// Files to be uploaded with POST
|
|
public $files;
|
|
|
|
// User set body contents
|
|
private $body = null;
|
|
|
|
// Library generated body that is regenerated through prepare method
|
|
private $prepared_body = null;
|
|
|
|
public function __construct($method, $url, $headers = array(), $body = null){
|
|
|
|
$this->params = new ParamStore();
|
|
$this->headers = new ParamStore();
|
|
|
|
// http params
|
|
$this->post = new ParamStore(null, true);
|
|
$this->get = new ParamStore(null, true);
|
|
|
|
$this->files = new ParamStore(null, true);
|
|
|
|
$this->setMethod($method);
|
|
$this->setUrl($url);
|
|
$this->setBody($body);
|
|
|
|
// make the request ready to be sent right from the start - prepare must be called manually from this point on if you ever add post or file parameters
|
|
$this->prepare();
|
|
}
|
|
|
|
/*
|
|
Does multiple things
|
|
- regenerate content body based on $post and $files parameters
|
|
- set content-type, content-length headers
|
|
- set transfer-encoding, expect headers
|
|
*/
|
|
public function prepare(){
|
|
|
|
/*
|
|
Any HTTP/1.1 message containing an entity-body SHOULD include a Content-Type header field defining the media type of that body.
|
|
http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
|
|
*/
|
|
|
|
// Must be a multipart request
|
|
if($this->files->all()){
|
|
|
|
$boundary = self::generateBoundary();
|
|
|
|
$this->prepared_body = Request::buildPostBody($this->post->all(), $this->files->all(), $boundary);
|
|
$this->headers->set('content-type', 'multipart/form-data; boundary='.$boundary);
|
|
|
|
} else if($this->post->all()){
|
|
|
|
$this->prepared_body = http_build_query($this->post->all());
|
|
$this->headers->set('content-type', 'application/x-www-form-urlencoded');
|
|
|
|
} else {
|
|
|
|
$this->headers->set('content-type', $this->detectContentType($this->body));
|
|
$this->prepared_body = $this->body;
|
|
}
|
|
|
|
/*
|
|
The transfer-length of a message is the length of the message-body as it appears in the message; that is, after any transfer-codings have been applied.
|
|
http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4
|
|
*/
|
|
|
|
$len = strlen($this->prepared_body);
|
|
|
|
if($len > 0){
|
|
$this->headers->set('content-length', $len);
|
|
} else {
|
|
$this->headers->remove('content-length');
|
|
$this->headers->remove('content-type');
|
|
}
|
|
}
|
|
|
|
public function __toString(){
|
|
$str = $this->getMethod().' '.$this->getUrl().' HTTP/'.$this->getProtocolVersion()."\r\n";
|
|
return $str.$this->getRawHeaders()."\r\n\r\n".$this->getRawBody();
|
|
}
|
|
|
|
public function setMethod($method){
|
|
$this->method = strtoupper($method);
|
|
}
|
|
|
|
public function getMethod(){
|
|
return $this->method;
|
|
}
|
|
|
|
// this was no longer working --- https://github.com/guzzle/psr7/blob/master/src/functions.php
|
|
public static function parseQuery($query){
|
|
$result = array();
|
|
parse_str($query, $result);
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function setUrl($url){
|
|
// remove hashtag - preg_replace so we don't have to check for its existence first - is it possible preserving hashtag?
|
|
$url = preg_replace('/#.*/', '', $url);
|
|
|
|
// check if url has any query parameters
|
|
$query = parse_url($url, PHP_URL_QUERY);
|
|
|
|
// remove it and add the query params to get collection
|
|
if($query){
|
|
//$url = str_replace('?'.$query, '', $url);
|
|
$url = preg_replace('/\?.*/', '', $url);
|
|
|
|
$result = self::parseQuery($query);
|
|
$this->get->replace($result);
|
|
}
|
|
|
|
// url without query params - those will be appended later
|
|
$this->url = $url;
|
|
$this->headers->set('host', parse_url($url, PHP_URL_HOST));
|
|
}
|
|
|
|
public function getRawHeaders(){
|
|
|
|
$result = array();
|
|
|
|
$headers = $this->headers->all();
|
|
|
|
// Sort headers by name
|
|
//ksort($headers);
|
|
|
|
// Turn this into name=value pairs
|
|
foreach($headers as $name => $values){
|
|
|
|
// could be an array if multiple headers are sent with the same name?
|
|
foreach( (array)$values as $value){
|
|
$name = implode('-', array_map('ucfirst', explode('-', $name)));
|
|
$result[] = sprintf("%s: %s", $name, $value);
|
|
}
|
|
}
|
|
|
|
return implode("\r\n", $result);
|
|
}
|
|
|
|
public function getUrl(){
|
|
|
|
// does this URL have any query parameters?
|
|
if($this->get->all()){
|
|
return $this->url.'?'.http_build_query($this->get->all());
|
|
}
|
|
|
|
return $this->url;
|
|
}
|
|
|
|
public function getUri(){
|
|
return call_user_func_array(array($this, "getUrl"), func_get_args());
|
|
}
|
|
|
|
public function setProtocolVersion($version){
|
|
$this->protocol_version = $version;
|
|
}
|
|
|
|
public function getProtocolVersion(){
|
|
return $this->protocol_version;
|
|
}
|
|
|
|
// Set raw contents of the body
|
|
// this will clear all the values currently stored in POST and FILES
|
|
// will be ignored during PREPARE if post or files contain any values
|
|
public function setBody($body, $content_type = false){
|
|
|
|
// clear old body data
|
|
$this->post->clear();
|
|
$this->files->clear();
|
|
|
|
// is this form data?
|
|
if(is_array($body)){
|
|
$body = http_build_query($body);
|
|
}
|
|
|
|
$this->body = (string)$body;
|
|
|
|
// plain text should be: text/plain; charset=UTF-8
|
|
if($content_type){
|
|
$this->headers->set('content-type', $content_type);
|
|
}
|
|
|
|
// do it!
|
|
$this->prepare();
|
|
}
|
|
|
|
private static function generateBoundary(){
|
|
return '-----'.md5(microtime().rand());
|
|
}
|
|
|
|
// can be $_POST and $_FILES
|
|
public static function buildPostBody($fields, $files, $boundary = null){
|
|
|
|
// the reason BODY part is not included in sprintf pattern is because of limits
|
|
$part_field = "--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n";
|
|
$part_file = "--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\nContent-Type: %s\r\n\r\n";
|
|
|
|
// each part should be preceeded by this line
|
|
if(!$boundary){
|
|
$boundary = self::generateBoundary();
|
|
}
|
|
|
|
$body = '';
|
|
|
|
foreach($fields as $name => $value){
|
|
$body .= sprintf($part_field, $boundary, $name, $value);
|
|
$body .= "{$value}\r\n";
|
|
}
|
|
|
|
// data better have [name, tmp_name, and optional type]
|
|
foreach($files as $name => $values) {
|
|
// Multiple files can be uploaded using different name for input.
|
|
// See http://php.net/manual/en/features.file-upload.multiple.php
|
|
if (!is_array($values['tmp_name'])) {
|
|
$multiValues = array_map(function ($a) {
|
|
return (array)$a;
|
|
}, $values);
|
|
$fieldName = $name;
|
|
} else {
|
|
$multiValues = $values;
|
|
$fieldName = "{$name}[]";
|
|
}
|
|
|
|
foreach (array_keys($multiValues['tmp_name']) as $key) {
|
|
|
|
// There must be no error http://php.net/manual/en/features.file-upload.errors.php
|
|
if (!$multiValues['tmp_name'][$key] || $multiValues['error'][$key] !== 0 || !is_readable($multiValues['tmp_name'][$key])) {
|
|
continue;
|
|
}
|
|
|
|
$body .= sprintf($part_file, $boundary, $fieldName, $multiValues['name'][$key], $multiValues['type'][$key]);
|
|
$body .= file_get_contents($multiValues['tmp_name'][$key]);
|
|
$body .= "\r\n";
|
|
}
|
|
}
|
|
$body .= "--{$boundary}--\r\n\r\n";
|
|
|
|
return $body;
|
|
}
|
|
|
|
private function detectContentType($data){
|
|
|
|
// http://www.w3.org/Protocols/rfc1341/4_Content-Type.html
|
|
|
|
// If the media type remains unknown, the recipient SHOULD treat it as type "application/octet-stream".
|
|
$content_type = 'application/octet-stream';
|
|
|
|
if(preg_match('/^{\s*"[^"]+"\s*:/', $data)){
|
|
$content_type = 'application/json';
|
|
} else if(preg_match('/^(?:<\?xml[^?>]+\?>)\s*<[^>]+>/i', $data)){
|
|
$content_type = 'application/xml';
|
|
} else if(preg_match('/^[a-zA-Z0-9_.~-]+=[^&]*&/', $data)){
|
|
$content_type = 'application/x-www-form-urlencoded';
|
|
}
|
|
|
|
return $content_type;
|
|
}
|
|
|
|
// Returns a parsed version of the body
|
|
/*
|
|
public function getBody(){
|
|
|
|
// what is the content type?
|
|
$content_type = $this->headers->get('content-type', '');
|
|
|
|
switch($content_type){
|
|
case 'application/x-www-form-urlencoded':
|
|
$result = array();
|
|
mb_parse_str($this->body, $result);
|
|
return $result;
|
|
case 'application/json':
|
|
return json_decode($this->body);
|
|
case 'text/xml':
|
|
case 'application/xml':
|
|
case 'application/x-xml':
|
|
return simplexml_load_string($this->body);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
*/
|
|
|
|
// Returns raw body string exactly as it appears in the HTTP request
|
|
public function getRawBody(){
|
|
return $this->prepared_body;
|
|
}
|
|
|
|
public static function createFromGlobals(){
|
|
|
|
$method = $_SERVER['REQUEST_METHOD'];
|
|
$scheme = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']) ? 'https' : 'http';
|
|
|
|
$url = $scheme.'://'. $_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
|
|
|
|
$request = new Request($method, $url);
|
|
|
|
// fill in headers
|
|
foreach($_SERVER as $name => $value){
|
|
|
|
if(strpos($name, 'HTTP_') === 0){
|
|
|
|
$name = substr($name, 5);
|
|
$name = str_replace('_', ' ', $name);
|
|
$name = ucwords(strtolower($name));
|
|
$name = str_replace(' ', '-', $name);
|
|
|
|
$request->headers->set($name, $value);
|
|
}
|
|
}
|
|
|
|
// for extra convenience
|
|
//$request->params->set('user-ip', $_SERVER['REMOTE_ADDR']);
|
|
|
|
// will be empty if content-type is multipart
|
|
$input = file_get_contents("php://input");
|
|
|
|
if(count($_FILES) > 0){
|
|
$request->post->replace($_POST);
|
|
$request->files->replace($_FILES);
|
|
} else if(count($_POST) > 0){
|
|
$request->post->replace($_POST);
|
|
} else {
|
|
$request->setBody($input);
|
|
}
|
|
|
|
// for extra convenience
|
|
$request->prepare();
|
|
|
|
return $request;
|
|
}
|
|
}
|
|
|
|
|
|
?>
|