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; } } ?>