. */ /** * This file contains the HTTP Request class. * * @author Dinu Florin * @package Modules * @subpackage Network */ /** * Request class. This class can make HTTP requests. * TODO: setCookie(), removeCookie(), getCookie(), post(HTTPPostData $data), options(), put(), delete(), get/setFollowRedirect(). * * @author Dinu Florin * @package Modules * @subpackage Network */ class HTTPRequest extends Pt { private $server = null; // The server private $port = 80; // The port private $protocol = null; // Protocol to be used private $headers = array(); private $cookies = array(); private $files = array(); // Temp files used. private $locks = array(); // Mutexes used to lock the files. const MAX_HEADER_PARSE = 20; // The number of response headers to parse in search of content length. const DEFAULT_BUFF_SIZE = 2048; // 2KB const MAX_BUFF_SIZE = 1048576; // 1MB /** * Constructor. * * @param string $server The server to connect to, eg. www.example.com. * @param string $port The port to use (default is 80). */ public function __construct($server, $port=80) { parent::__construct(); assert('is_string($server)'); assert('preg_match("/[-a-z0-9.]+/i", $server)'); assert('is_numeric($port)'); assert('$port > 0'); $this->server = $server; $this->port = $port; $this->protocol = HTTP::HTTP1_1; Pt::registerSlot('HTTPRequest', 'cleanup'); // Pt::connect('Core', 'shutdown', $this, 'cleanup'); } /** * Add a HTTP header. If the header already exists it will not be replaced, another one is added. * * @param string $name The header name. * @param string $value The header value. * @see HTTPRequest::setHeader() */ public function addHeader($name, $value) { assert('is_string($name)'); $name = HTTP::makeStdHeaderName($name); if(!isset($this->headers[$name])) { $this->headers[$name] = (string)$value; } else { $this->headers[$name] .= ', '.(string)$value; } } /** * Set a header. If the header exists it will be replaced. * * @param string $name The header name. * @param string $value The header value. * @see HTTPRequest::addHeader() */ public function setHeader($name, $value) { assert('is_string($name)'); $name = HTTP::makeStdHeaderName($name); $this->headers[$name] = (string)$value; } /** * Remove a header. * * @param string $name The header name. * @return mixed The header value. This will be an array if there are multiple headers or a string just for one. */ public function removeHeader($name) { assert('is_string($name)'); $name = HTTP::makeStdHeaderName($name); if(isset($this->headers[$name])) { $val = $this->headers[$name]; unset($this->headers[$name]); } else { $val = null; } return $val; } /** * Get a header. * * @param string $name The header name. * @return mixed The header value. This will be an array if there are multiple headers with the same name, * a string if there is only one header or null if there is no header with the supplied name. */ public function getHeader($name) { assert('is_string($name)'); $name = HTTP::makeStdHeaderName($name); if(isset($this->headers[$name])) { return $this->headers[$name]; } else { return null; } } /** * Get the request headers as an array of key-value pairs of headers. * For example: array('Content-type'=>'text/plain'); * * @return array */ public function getHeaders() { return $this->headers; } /** * Perform a GET request. * * @param string $url The URL of the resource. Eg. /path/to/the/resource * @return HTTPResponse or HTTPBigResponse */ public function get($uri) { $request = "GET {$uri} {$this->protocol}\r\n"; $request .= $this->buildHeaders(); return $this->read($request); } /** * Read the response. If the response size is bigger then HTTPRequest::MAX_BUFF_SIZE it will be saved to a file. * * @param string $request The HTTP request to send to the server. * @return HTTPResponse or HTTPBigResponse */ private function read($request) { $socket = fsockopen($this->server, $this->port, $errno, $errstr); if($socket) { //Write the headers and stuff to the socket. fwrite($socket, $request); fwrite($socket, "\n\r"); //Start reading the response, it's a little complicated because it could be something big. $response = ''; $toFile = false; $fileName = null; $file = null; $size = 0; //First go through the headers and look for content-length while(!feof($socket)) { $header = fgets($socket); $response .= $header; $header = explode(':', $header); if(count($header) == 2) //It's a header { $header[0] = trim($header[0]); $header[1] = trim($header[1]); if(strcasecmp($header[0], 'content-length') == 0 && (int)$header[1] > 0) { $size = (int)$header[1]; break; } } elseif($header == "\r\n") //It's the empty line before the body, there is no Content-length header { break; } } if($size > 0 && $size + strlen($response) < self::MAX_BUFF_SIZE) //We found the content length and it's ok { while(!feof($socket)) { $response .= fread($socket, $size); } } else //We need to be careful not to exceed MAX_BUFF_SIZE { while(!feof($socket)) { if(!$toFile) { $response .= fread($socket, self::MAX_BUFF_SIZE); if(strlen($response) > self::MAX_BUFF_SIZE) // The response is too large, we need to put it in a temp file. { //This block will only be executed once because $toFile is set to true //and the first if after the while branches to the else. $fileName = Util::getTempDir().DIRECTORY_SEPARATOR.md5($this->server.$this->port.$request); $toFile = true; // Lock the file $mutex = new Mutex($fileName); $mutex->lock(); $file = fopen($fileName, 'wb+'); if($file) { fwrite($file, $response); unset($response); // Save the lock and the file, they will be cleaned up at te end of the request $this->locks[] = $mutex; $this->files[] = $fileName; } else { $lock->unlock(); throw new NetworkException('Unable to open temp file'); } } } else { $buff = fread($socket, self::MAX_BUFF_SIZE); fwrite($file, $buff); } } if($toFile) { fclose($file); } } fclose($socket); } else { throw new NetworkException('Unable to open socket'); } if($toFile) { return new HTTPBigResponse($fileName); } else { return new HTTPResponse($response); } } /** * Build a string from the headers array. * * @return string */ private function buildHeaders() { $val = "Host: {$this->server}\r\n"; if(is_null($this->getHeader('User-Agent'))) { $val .= "User-Agent: Pt (http://pt-toolkit.googlecode.com/)\r\n"; } foreach($this->headers as $name=>$value) { $val .= "{$name}: {$value}\r\n"; } $val .= "Connection: close\r\n"; return $val; } /** * Cleanup the files and unlock the locks used by this request. * * @slot */ public function cleanup() { foreach($this->files as $file) { @unlink($file); } foreach($this->locks as $lock) { $lock->unlock(); } } } ?>