diff --git a/src/pjdietz/WellRESTed/Message.php b/src/pjdietz/WellRESTed/Message.php index 4976489..8596cc0 100644 --- a/src/pjdietz/WellRESTed/Message.php +++ b/src/pjdietz/WellRESTed/Message.php @@ -27,10 +27,6 @@ abstract class Message * @var array */ protected $headerLookup; - /** @var string Name of the protocol to use. */ - protected $protocol = 'HTTP'; - /** @var string Version of the protocol to use. */ - protected $protocolVersion = '1.1'; // ------------------------------------------------------------------------- @@ -66,22 +62,6 @@ abstract class Message $this->body = $body; } - /** - * Return if the body is set - * - * @return bool - */ - public function issetBody() - { - return isset($this->body); - } - - /** Unset the body property */ - public function unsetBody() - { - unset($this->body); - } - /** * Return an associative array of all set headers. * @@ -110,23 +90,16 @@ abstract class Message * Return the value of a given header, or false if it does not exist. * * @param string $name - * @return string|bool + * @return string|null */ public function getHeader($name) { $lowerName = strtolower($name); - if (isset($this->headerLookup[$lowerName])) { - $realName = $this->headerLookup[$lowerName]; - - if (isset($this->headers[$realName])) { - return $this->headers[$realName]; - } - + return $this->headers[$realName]; } - - return false; + return null; } /** @@ -188,90 +161,4 @@ abstract class Message } } - - /** - * Return the protocol (e.g., HTTP) - * - * @return string - */ - public function getProtocol() - { - return $this->protocol; - } - - /** - * Set the protocol for the message. - * - * The value is expected to be the name of the protocol only. If the - * version is included, the version is striped and set as the - * protocolVersion property. - * - * - * $instance->protocol = 'HTTP1/1'; - * print $instance->protocol; // 'HTTP'; - * print $instance->protocolVersion; // '1.1'; - * - * - * @param $protocol - */ - public function setProtocol($protocol) - { - if (strpos($protocol, '/') === false) { - list($this->protocol, $this->protocolVersion) = explode('/', $protocol, 2); - } else { - $this->protocol = $protocol; - } - } - - /** - * Return if the protocol property is set. - * - * @return bool - */ - public function issetProtocol() - { - return isset($this->protocol); - } - - /** Unset the protocol property. */ - public function unsetProtocol() - { - unset($this->protocol); - } - - /** - * Return the version portion of the protocol. For HTTP/1.1, this is 1.1 - * - * @return string - */ - public function getProtocolVersion() - { - return $this->protocolVersion; - } - - /** - * Assign a new protocol version - * - * @param string $protocolVersion - */ - public function setProtocolVersion($protocolVersion) - { - $this->protocolVersion = $protocolVersion; - } - - /** - * Return if the version portion of the protocol is set. - * - * @return bool - */ - public function issetProtocolVersion() - { - return isset($this->protocolVersion); - } - - /** Unset the version portion of the protocol. */ - public function unsetProtocolVersion() - { - unset($this->protocolVersion); - } } diff --git a/src/pjdietz/WellRESTed/Request.php b/src/pjdietz/WellRESTed/Request.php index a8e3e0d..f57b4b0 100644 --- a/src/pjdietz/WellRESTed/Request.php +++ b/src/pjdietz/WellRESTed/Request.php @@ -10,8 +10,10 @@ namespace pjdietz\WellRESTed; +use InvalidArgumentException; use pjdietz\WellRESTed\Exceptions\CurlException; use pjdietz\WellRESTed\Interfaces\RequestInterface; +use UnexpectedValueException; /** * A Request instance represents an HTTP request. This class has two main uses: @@ -32,20 +34,20 @@ class Request extends Message implements RequestInterface * @static */ static protected $theRequest; - /** @var string The Hostname for the request (e.g., www.google.com) */ - private $hostname; /** @var string HTTP method or verb for the request */ - private $method = 'GET'; + private $method = "GET"; + /** @var string The Hostname for the request (e.g., www.google.com) */ + private $hostname = "localhost"; + /** @var string Scheme for the request (Must be "http" or "https" */ + protected $scheme; /** @var string Path component of the URI for the request */ private $path = '/'; /** @var array Array of fragments of the path, delimited by slashes */ private $pathParts; - /** @var int */ + /** @var int HTTP Port*/ private $port = 80; - /**@var array Associative array of query parameters */ + /** @var array Associative array of query parameters */ private $query; - /** @var int internal count of the number of times routers have dispatched this instance */ - private $routeDepth = 0; // ------------------------------------------------------------------------- @@ -55,7 +57,7 @@ class Request extends Message implements RequestInterface * @param string|null $uri * @param string $method */ - public function __construct($uri = null, $method = 'GET') + public function __construct($uri = null, $method = "GET") { parent::__construct(); if (!is_null($uri)) { @@ -83,7 +85,11 @@ class Request extends Message implements RequestInterface return self::$theRequest; } - /** @return array all request headers from the current request. */ + /** + * Read and return all request headers from the request issued to the server. + * + * @return array Associative array of headers + */ public static function getRequestHeaders() { if (function_exists('apache_request_headers')) { @@ -138,24 +144,6 @@ class Request extends Message implements RequestInterface $this->hostname = $hostname; } - /** - * Return if the hostname portion of the URI is set. - * - * @return bool - */ - public function issetHostName() - { - return isset($this->hostname); - } - - /** - * Unset the hostname portion of the URI. - */ - public function unsetHostname() - { - unset($this->hostname); - } - /** * Return the method (e.g., GET, POST, PUT, DELETE) * @@ -194,7 +182,11 @@ class Request extends Message implements RequestInterface public function setPath($path) { $this->path = $path; - $this->pathParts = explode('/', substr($path, 1)); + if ($path !== "/") { + $this->pathParts = explode("/", substr($path, 1)); + } else { + $this->pathParts = array(); + } } /** @@ -207,15 +199,26 @@ class Request extends Message implements RequestInterface return $this->pathParts; } - /** @return int */ + /** + * Return the HTTP port + * + * @return int + */ public function getPort() { return $this->port; } - /** @param int $port */ - public function setPort($port) + /** + * Set the HTTP port + * + * @param int $port + */ + public function setPort($port = null) { + if (is_null($port)) { + $port = $this->getDefaultPort(); + } $this->port = $port; } @@ -234,22 +237,50 @@ class Request extends Message implements RequestInterface * joined by ampersands or it can be an associative array. * * @param string|array $query - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ public function setQuery($query) { if (is_string($query)) { $qs = $query; parse_str($qs, $query); + } elseif (is_object($query)) { + $query = (array) $query; } if (is_array($query)) { + ksort($query); $this->query = $query; } else { - throw new \InvalidArgumentException('Unable to parse query string.'); + throw new InvalidArgumentException('Unable to parse query string.'); } } + /** + * Set the scheme for the request (either "http" or "https") + * + * @param string $scheme + * @throws \UnexpectedValueException + */ + public function setScheme($scheme) + { + $scheme = strtolower($scheme); + if (!in_array($scheme, array("http","https"))) { + throw new UnexpectedValueException('Scheme must be "http" or "https".'); + } + $this->scheme = $scheme; + } + + /** + * Return the scheme for the request (either "http" or "https") + * + * @return string + */ + public function getScheme() + { + return $this->scheme; + } + /** * Return the full URI includeing protocol, hostname, path, and query. * @@ -257,18 +288,16 @@ class Request extends Message implements RequestInterface */ public function getUri() { - $uri = strtolower($this->protocol) . '://' . $this->hostname; - - if ($this->port !== 80) { + $uri = $this->scheme . "://" . $this->hostname; + if ($this->port !== $this->getDefaultPort()) { $uri .= ':' . $this->port; } - - $uri .= $this->path; - + if ($this->path !== "/") { + $uri .= $this->path; + } if ($this->query) { $uri .= '?' . http_build_query($this->query); } - return $uri; } @@ -280,37 +309,33 @@ class Request extends Message implements RequestInterface */ public function setUri($uri) { + // Provide http and localhost if missing. + if ($uri[0] === "/") { + $uri = "http://localhost" . $uri; + } elseif (strpos($uri, "://") === false) { + $uri = "http://" . $uri; + } + $parsed = parse_url($uri); - $host = isset($parsed['host']) ? $parsed['host'] : ''; + $scheme = isset($parsed["scheme"]) ? $parsed["scheme"] : "http"; + $this->setScheme($scheme); + + $host = isset($parsed['host']) ? $parsed['host'] : 'localhost'; $this->setHostname($host); - $path = isset($parsed['path']) ? $parsed['path'] : ''; - $this->setPath($path); - - $port = isset($parsed['port']) ? (int) $parsed['port'] : 80; + $port = isset($parsed['port']) ? (int) $parsed['port'] : $this->getDefaultPort(); $this->setPort($port); + $path = isset($parsed['path']) ? $parsed['path'] : '/'; + $this->setPath($path); + $query = isset($parsed['query']) ? $parsed['query'] : ''; $this->setQuery($query); } // ------------------------------------------------------------------------- - /** @return int The number of times a router has dispatched this Routable */ - public function getRouteDepth() - { - return $this->routeDepth; - } - - /** Increase the instance's internal count of its depth in nested route tables */ - public function incrementRouteDepth() - { - $this->routeDepth++; - } - - // ------------------------------------------------------------------------- - /** * Make a cURL request out of the instance and return a Response. * @@ -396,4 +421,13 @@ class Request extends Message implements RequestInterface return $resp; } + /** + * Return the default port for the currently set scheme. + * + * @return int; + */ + protected function getDefaultPort() + { + return $this->scheme === "http" ? 80 : 443; + } } diff --git a/test/RequestTest.php b/test/RequestTest.php new file mode 100644 index 0000000..2dd165a --- /dev/null +++ b/test/RequestTest.php @@ -0,0 +1,524 @@ +request = new Request(); + foreach ($this->headerProvider() as $item) { + $name = $item[0]; + $value = $item[1]; + $this->request->setHeader($name, $value); + } + } + + public function headerProvider() + { + return array( + array("Accept-Charset", "utf-8", "accept-charset"), + array("Accept-Encoding", "gzip, deflate", "ACCEPT-ENCODING"), + array("Cache-Control", "no-cache", "Cache-Control"), + ); + } + + public function testSetBody() + { + $body = "This is the body"; + $rqst = new Request(); + $rqst->setBody($body); + $this->assertEquals($body, $rqst->getBody()); + } + + public function testNullBody() + { + $this->assertNull($this->request->getBody()); + } + + public function testHeaders() + { + $this->assertEquals(3, count($this->request->getHeaders())); + } + + /** + * @dataProvider headerProvider + */ + public function testHeaderLines($name, $value, $testName) + { + $line = "$name: $value"; + $this->assertTrue(in_array($line, $this->request->getHeaderLines())); + } + + /** + * @dataProvider headerProvider + */ + public function testHeaderValue($name, $value, $testName) + { + $this->assertEquals($value, $this->request->getHeader($testName)); + } + + /** + * @dataProvider headerProvider + */ + public function testNonsetHeader() + { + $this->assertNull($this->request->getHeader("no-header")); + } + + /** + * @dataProvider headerProvider + */ + public function testUnsetHeader($name, $value, $testName) + { + $this->request->unsetHeader($testName); + $this->assertNull($this->request->getHeader($testName)); + } + + /** + * @dataProvider headerProvider + */ + public function testUpdateHeader($name, $value, $testName) + { + $newvalue = "newvalue"; + $this->request->setHeader($testName, "newvalue"); + $this->assertEquals($newvalue, $this->request->getHeader($testName)); + } + + /** + * @dataProvider headerProvider + */ + public function testIssetHeader($name, $value, $testName) + { + $this->assertTrue($this->request->issetHeader($testName)); + } + + /** + * @dataProvider headerProvider + */ + public function testNotIssetHeader($name, $value, $testName) + { + $this->request->unsetHeader($testName); + $this->assertFalse($this->request->issetHeader($testName)); + } + + /** + * @dataProvider uriProvider + */ + public function testUri($uri, $data) + { + $rqst = new Request($uri); + $this->assertEquals($data->uri, $rqst->getUri()); + } + + /** + * @dataProvider uriProvider + */ + public function testScheme($uri, $data) + { + $rqst = new Request($uri); + $this->assertEquals($data->scheme, $rqst->getScheme()); + } + + /** + * @dataProvider uriProvider + */ + public function testHostname($uri, $data) + { + $rqst = new Request($uri); + $this->assertEquals($data->hostname, $rqst->getHostname()); + } + + /** + * @dataProvider uriProvider + */ + public function testPort($uri, $data) + { + $rqst = new Request($uri); + $this->assertEquals($data->port, $rqst->getPort()); + } + + /** + * @dataProvider uriProvider + */ + public function testPath($uri, $data) + { + $rqst = new Request($uri); + $this->assertEquals($data->path, $rqst->getPath()); + } + + /** + * @dataProvider uriProvider + */ + public function testPathParts($uri, $data) + { + $rqst = new Request($uri); + $this->assertEquals($data->parts, $rqst->getPathParts()); + } + + /** + * @dataProvider uriProvider + */ + public function testQuery($uri, $data) + { + $rqst = new Request($uri); + $this->assertEquals($data->query, $rqst->getQuery()); + } + + public function uriProvider() + { + return array( + array( + "http://www.google.com", + (object) [ + "uri" => "http://www.google.com", + "scheme" => "http", + "hostname" => "www.google.com", + "port" => 80, + "path" => "/", + "query" => [], + "parts" => [] + ] + ), + array( + "https://www.google.com", + (object) [ + "uri" => "https://www.google.com", + "scheme" => "https", + "hostname" => "www.google.com", + "port" => 443, + "path" => "/", + "query" => [], + "parts" => [] + ] + ), + array( + "localhost:8080/my/path/with/parts", + (object) [ + "uri" => "http://localhost:8080/my/path/with/parts", + "scheme" => "http", + "hostname" => "localhost", + "port" => 8080, + "path" => "/my/path/with/parts", + "query" => [], + "parts" => ["my", "path", "with", "parts"] + ] + ), + array( + "localhost?dog=bear&cat=molly", + (object) [ + "uri" => "http://localhost?cat=molly&dog=bear", + "scheme" => "http", + "hostname" => "localhost", + "port" => 80, + "path" => "/", + "query" => [ + "cat" => "molly", + "dog" => "bear" + ], + "parts" => [] + ] + ), + array( + "/my-page?id=2", + (object) [ + "uri" => "http://localhost/my-page?id=2", + "scheme" => "http", + "hostname" => "localhost", + "port" => 80, + "path" => "/my-page", + "query" => [ + "id" => "2" + ], + "parts" => ["my-page"] + ] + ) + ); + } + + /** + * @dataProvider invalidSchemeProvider + * @expectedException \UnexpectedValueException + */ + public function testInvalidScheme($scheme) + { + $this->request->setScheme($scheme); + } + + public function invalidSchemeProvider() + { + return [ + [""], + ["ftp"], + ["ssh"], + [null], + [0] + ]; + } + + /** + * @dataProvider queryProvider + */ + public function testSetQuery($input, $expected) + { + $this->request->setQuery($input); + $this->assertEquals($expected, $this->request->getQuery()); + } + + public function queryProvider() + { + return [ + [ + "cat=molly&dog=bear", + [ + "cat" => "molly", + "dog" => "bear" + ] + ], + [ + ["id" => "1"], + ["id" => "1"] + ], + [ + (object)["dog" => "bear"], + ["dog" => "bear"] + ], + ["", []], + [[], []], + ]; + } + + /** + * @dataProvider invalidQueryProvider + * @expectedException \InvalidArgumentException + */ + public function testInvalidQuery($query) + { + $this->request->setQuery($query); + } + + public function invalidQueryProvider() + { + return [ + [11], + [false], + [true], + [null] + ]; + } + + /** + * @dataProvider methodProvider + */ + public function testMethod($method) + { + $this->request->setMethod($method); + $this->assertEquals($method, $this->request->getMethod()); + } + + public function methodProvider() + { + return array( + array("GET"), + array("POST"), + array("PUT"), + array("DELETE"), + array("OPTIONS"), + array("HEAD") + ); + } + + /** + * @dataProvider serverProvider + */ + public function testServerRequestMethod($serverVars, $expected) + { + $original = $_SERVER; + $_SERVER = array_merge($_SERVER, $serverVars); + $rqst = new Request(); + $rqst->readHttpRequest(); + $this->assertEquals($expected->method, $rqst->getMethod()); + $_SERVER = $original; + } + + /** + * @dataProvider serverProvider + */ + public function testServerRequestHost($serverVars, $expected) + { + $original = $_SERVER; + $_SERVER = array_merge($_SERVER, $serverVars); + $rqst = new Request(); + $rqst->readHttpRequest(); + $this->assertEquals($expected->host, $rqst->getHostname()); + $_SERVER = $original; + } + + /** + * @dataProvider serverProvider + */ + public function testServerRequestPath($serverVars, $expected) + { + $original = $_SERVER; + $_SERVER = array_merge($_SERVER, $serverVars); + $rqst = new Request(); + $rqst->readHttpRequest(); + $this->assertEquals($expected->path, $rqst->getPath()); + $_SERVER = $original; + } + + /** + * @dataProvider serverProvider + */ + public function testServerRequestHeaders($serverVars, $expected) + { + $original = $_SERVER; + $_SERVER = array_merge($_SERVER, $serverVars); + $rqst = new Request(); + $rqst->readHttpRequest(); + foreach ($expected->headers as $name => $value) { + $this->assertEquals($value, $rqst->getHeader($name)); + } + $_SERVER = $original; + } + + /** + * @dataProvider serverProvider + */ + public function testHasApacheHeaders($serverVars, $expected) + { + if (!function_exists('apache_request_headers')) { + function apache_request_headers() { + $headers = ''; + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) === 'HTTP_') { + $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + } + } + return $headers; + } + } + + $original = $_SERVER; + $_SERVER = array_merge($_SERVER, $serverVars); + $rqst = new Request(); + $rqst->readHttpRequest(); + foreach ($expected->headers as $name => $value) { + $this->assertEquals($value, $rqst->getHeader($name)); + } + $_SERVER = $original; + } + + /** + * We can only test the static member once, so no need for dataProvider. + */ + public function testStaticRequest() + { + $data = $this->serverProvider(); + $serverVars = $data[0][0]; + $expected = $data[0][1]; + + $original = $_SERVER; + $_SERVER = array_merge($_SERVER, $serverVars); + $rqst = Request::getRequest(); + $this->assertEquals($expected->host, $rqst->getHostname()); + + $_SERVER = $original; + + return $rqst; + } + + /** + * @depends testStaticRequest + */ + public function testStaticRequestAgain($previousRequest) + { + $rqst = Request::getRequest(); + $this->assertSame($previousRequest, $rqst); + } + + public function serverProvider() + { + return [ + [ + [ + "REQUEST_METHOD" => "GET", + "REQUEST_URI" => "/", + "HTTP_ACCEPT_CHARSET" => "utf-8", + "HTTP_HOST" => "localhost" + ], + (object) [ + "method" => "GET", + "host" => "localhost", + "path" => "/", + "headers" => [ + "Accept-charset" => "utf-8" + ] + ] + ], + [ + [ + "REQUEST_METHOD" => "POST", + "REQUEST_URI" => "/my/page", + "HTTP_ACCEPT_CHARSET" => "utf-8", + "HTTP_HOST" => "mysite.com" + ], + (object) [ + "method" => "POST", + "host" => "mysite.com", + "path" => "/my/page", + "headers" => [ + "Accept-charset" => "utf-8" + ] + ] + ] + ]; + } + + /** + * @dataProvider curlProvider + */ + public function testCurl($method, $uri, $opts, $code) + { + $rqst = new Request($uri); + $rqst->setMethod($method); + $resp = $rqst->request($opts); + $this->assertEquals($code, $resp->getStatusCode()); + } + + public function curlProvider() + { + return [ + ["GET", "http://icanhasip.com", [ + [CURLOPT_MAXREDIRS => 2] + ], 200], + ["POST", "http://icanhasip.com", [], 200], + ["PUT", "http://icanhasip.com", [], 405], + ["DELETE", "http://icanhasip.com", [], 405] + ]; + } + + /** + * @dataProvider curlErrorProvider + * @expectedException \pjdietz\WellRESTed\Exceptions\CurlException + */ + public function testErrorCurl($uri, $opts) + { + $rqst = new Request($uri); + $resp = $rqst->request($opts); + } + + public function curlErrorProvider() + { + return [ + ["http://localhost:9991", [ + CURLOPT_FAILONERROR, true + ]], + ]; + } + +}