diff --git a/src/Http/Context.php b/src/Http/Context.php index 86301b98..d1073eb7 100644 --- a/src/Http/Context.php +++ b/src/Http/Context.php @@ -37,14 +37,14 @@ public function __construct(IRequest $request, IResponse $response) /** * Attempts to cache the sent entity by its last modification date. - * @param string|int|DateTime last modified time + * @param string|int|\DateTime last modified time * @param string strong entity tag validator * @return bool */ public function isModified($lastModified = NULL, $etag = NULL) { if ($lastModified) { - $this->response->setHeader('Last-Modified', $this->response->date($lastModified)); + $this->response->setHeader('Last-Modified', Helpers::formatDate($lastModified)); } if ($etag) { $this->response->setHeader('ETag', '"' . addslashes($etag) . '"'); diff --git a/src/Http/FileUpload.php b/src/Http/FileUpload.php index 95f8bf5f..4a188302 100644 --- a/src/Http/FileUpload.php +++ b/src/Http/FileUpload.php @@ -17,14 +17,14 @@ * * @property-read string $name * @property-read string $sanitizedName - * @property-read string $contentType + * @property-read string|NULL $contentType * @property-read int $size * @property-read string $temporaryFile * @property-read int $error * @property-read bool $ok * @property-read bool $image - * @property-read array $imageSize - * @property-read string $contents + * @property-read array|NULL $imageSize + * @property-read string|NULL $contents */ class FileUpload extends Nette\Object { @@ -81,7 +81,7 @@ public function getSanitizedName() /** * Returns the MIME content type of an uploaded file. - * @return string + * @return string|NULL */ public function getContentType() { @@ -173,6 +173,7 @@ public function isImage() /** * Returns the image. * @return Nette\Utils\Image + * @throws Nette\Utils\ImageException */ public function toImage() { @@ -182,7 +183,7 @@ public function toImage() /** * Returns the dimensions of an uploaded image as array. - * @return array + * @return array|NULL */ public function getImageSize() { @@ -192,7 +193,7 @@ public function getImageSize() /** * Get file contents. - * @return string + * @return string|NULL */ public function getContents() { diff --git a/src/Http/Helpers.php b/src/Http/Helpers.php index 0cad5293..ef7f7946 100644 --- a/src/Http/Helpers.php +++ b/src/Http/Helpers.php @@ -7,7 +7,8 @@ namespace Nette\Http; -use Nette; +use Nette, + Nette\Utils\DateTime; /** @@ -18,6 +19,19 @@ class Helpers { + /** + * Returns HTTP valid date format. + * @param string|int|\DateTime + * @return string + */ + public static function formatDate($time) + { + $time = DateTime::from($time); + $time->setTimezone(new \DateTimeZone('GMT')); + return $time->format('D, d M Y H:i:s \G\M\T'); + } + + /** * Is IP address in CIDR block? * @return bool diff --git a/src/Http/IRequest.php b/src/Http/IRequest.php index f855436d..338f4e90 100644 --- a/src/Http/IRequest.php +++ b/src/Http/IRequest.php @@ -54,7 +54,7 @@ function getPost($key = NULL, $default = NULL); /** * Returns uploaded file. * @param string key (or more keys) - * @return FileUpload + * @return FileUpload|NULL */ function getFile($key); @@ -122,19 +122,19 @@ function isAjax(); /** * Returns the IP address of the remote client. - * @return string + * @return string|NULL */ function getRemoteAddress(); /** * Returns the host of the remote client. - * @return string + * @return string|NULL */ function getRemoteHost(); /** * Returns raw content of HTTP request body. - * @return string + * @return string|NULL */ function getRawBody(); diff --git a/src/Http/IResponse.php b/src/Http/IResponse.php index e0c40703..a3c7eecd 100644 --- a/src/Http/IResponse.php +++ b/src/Http/IResponse.php @@ -115,7 +115,7 @@ function redirect($url, $code = self::S302_FOUND); /** * Sets the number of seconds before a page cached on a browser expires. - * @param mixed timestamp or number of seconds + * @param string|int|\DateTime time, value 0 means "until the browser is closed" * @return void */ function setExpiration($seconds); @@ -126,9 +126,17 @@ function setExpiration($seconds); */ function isSent(); + /** + * Returns value of an HTTP header. + * @param string + * @param mixed + * @return mixed + */ + function getHeader($header, $default = NULL); + /** * Returns a list of headers to sent. - * @return array + * @return array (name => value) */ function getHeaders(); diff --git a/src/Http/Request.php b/src/Http/Request.php index 44a97aba..d40eab72 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -16,8 +16,8 @@ * @author David Grudl * * @property-read UrlScript $url - * @property-read mixed $query - * @property-read bool $post + * @property-read array $query + * @property-read array $post * @property-read array $files * @property-read array $cookies * @property-read string $method @@ -25,9 +25,9 @@ * @property-read Url|NULL $referer * @property-read bool $secured * @property-read bool $ajax - * @property-read string $remoteAddress - * @property-read string $remoteHost - * @property-read string $rawBody + * @property-read string|NULL $remoteAddress + * @property-read string|NULL $remoteHost + * @property-read string|NULL $rawBody */ class Request extends Nette\Object implements IRequest { @@ -52,22 +52,22 @@ class Request extends Nette\Object implements IRequest /** @var array */ private $headers; - /** @var string */ + /** @var string|NULL */ private $remoteAddress; - /** @var string */ + /** @var string|NULL */ private $remoteHost; - /** @var string */ - private $rawBody; + /** @var callable|NULL */ + private $rawBodyCallback; public function __construct(UrlScript $url, $query = NULL, $post = NULL, $files = NULL, $cookies = NULL, - $headers = NULL, $method = NULL, $remoteAddress = NULL, $remoteHost = NULL) + $headers = NULL, $method = NULL, $remoteAddress = NULL, $remoteHost = NULL, $rawBodyCallback = NULL) { $this->url = $url; if ($query === NULL) { - parse_str($url->query, $this->query); + parse_str($url->getQuery(), $this->query); } else { $this->query = (array) $query; } @@ -75,9 +75,10 @@ public function __construct(UrlScript $url, $query = NULL, $post = NULL, $files $this->files = (array) $files; $this->cookies = (array) $cookies; $this->headers = array_change_key_case((array) $headers, CASE_LOWER); - $this->method = $method; + $this->method = $method ?: 'GET'; $this->remoteAddress = $remoteAddress; $this->remoteHost = $remoteHost; + $this->rawBodyCallback = $rawBodyCallback; } @@ -139,7 +140,7 @@ public function getPost($key = NULL, $default = NULL) /** * Returns uploaded file. * @param string key (or more keys) - * @return FileUpload + * @return FileUpload|NULL */ public function getFile($key) { @@ -204,8 +205,7 @@ public function isMethod($method) /** - * Checks if the request method is POST. - * @return bool + * @deprecated */ public function isPost() { @@ -223,11 +223,7 @@ public function isPost() public function getHeader($header, $default = NULL) { $header = strtolower($header); - if (isset($this->headers[$header])) { - return $this->headers[$header]; - } else { - return $default; - } + return isset($this->headers[$header]) ? $this->headers[$header] : $default; } @@ -257,7 +253,7 @@ public function getReferer() */ public function isSecured() { - return $this->url->scheme === 'https'; + return $this->url->getScheme() === 'https'; } @@ -273,7 +269,7 @@ public function isAjax() /** * Returns the IP address of the remote client. - * @return string + * @return string|NULL */ public function getRemoteAddress() { @@ -283,12 +279,12 @@ public function getRemoteAddress() /** * Returns the host of the remote client. - * @return string + * @return string|NULL */ public function getRemoteHost() { - if (!$this->remoteHost) { - $this->remoteHost = $this->remoteAddress ? getHostByAddr($this->remoteAddress) : NULL; + if ($this->remoteHost === NULL && $this->remoteAddress !== NULL) { + $this->remoteHost = getHostByAddr($this->remoteAddress); } return $this->remoteHost; } @@ -296,25 +292,18 @@ public function getRemoteHost() /** * Returns raw content of HTTP request body. - * @return string + * @return string|NULL */ public function getRawBody() { - if (PHP_VERSION_ID >= 50600) { - return file_get_contents('php://input'); - - } elseif ($this->rawBody === NULL) { // can be read only once in PHP < 5.6 - $this->rawBody = (string) file_get_contents('php://input'); - } - - return $this->rawBody; + return $this->rawBodyCallback ? call_user_func($this->rawBodyCallback) : NULL; } /** - * Parse Accept-Language header and returns prefered language. - * @param array Supported languages - * @return string|null + * Parse Accept-Language header and returns preferred language. + * @param string[] supported languages + * @return string|NULL */ public function detectLanguage(array $langs) { diff --git a/src/Http/RequestFactory.php b/src/Http/RequestFactory.php index 19d9e799..7752654e 100644 --- a/src/Http/RequestFactory.php +++ b/src/Http/RequestFactory.php @@ -64,19 +64,19 @@ public function createHttpRequest() { // DETECTS URI, base path and script path of the request. $url = new UrlScript; - $url->scheme = !empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https' : 'http'; - $url->user = isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : ''; - $url->password = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : ''; + $url->setScheme(!empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off') ? 'https' : 'http'); + $url->setUser(isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : ''); + $url->setPassword(isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : ''); // host & port if ((isset($_SERVER[$tmp = 'HTTP_HOST']) || isset($_SERVER[$tmp = 'SERVER_NAME'])) && preg_match('#^([a-z0-9_.-]+|\[[a-f0-9:]+\])(:\d+)?\z#i', $_SERVER[$tmp], $pair) ) { - $url->host = strtolower($pair[1]); + $url->setHost(strtolower($pair[1])); if (isset($pair[2])) { - $url->port = (int) substr($pair[2], 1); + $url->setPort(substr($pair[2], 1)); } elseif (isset($_SERVER['SERVER_PORT'])) { - $url->port = (int) $_SERVER['SERVER_PORT']; + $url->setPort($_SERVER['SERVER_PORT']); } } @@ -95,12 +95,12 @@ public function createHttpRequest() $requestUrl = Strings::replace($requestUrl, $this->urlFilters['url']); $tmp = explode('?', $requestUrl, 2); - $url->path = Strings::replace($tmp[0], $this->urlFilters['path']); - $url->query = isset($tmp[1]) ? $tmp[1] : ''; + $url->setPath(Strings::replace($tmp[0], $this->urlFilters['path'])); + $url->setQuery(isset($tmp[1]) ? $tmp[1] : ''); // normalized url $url->canonicalize(); - $url->path = Strings::fixEncoding($url->path); + $url->setPath(Strings::fixEncoding($url->getPath())); // detect script path if (isset($_SERVER['SCRIPT_NAME'])) { @@ -113,21 +113,21 @@ public function createHttpRequest() $script = '/'; } - $path = strtolower($url->path) . '/'; + $path = strtolower($url->getPath()) . '/'; $script = strtolower($script) . '/'; $max = min(strlen($path), strlen($script)); for ($i = 0; $i < $max; $i++) { if ($path[$i] !== $script[$i]) { break; } elseif ($path[$i] === '/') { - $url->scriptPath = substr($url->path, 0, $i + 1); + $url->setScriptPath(substr($url->getPath(), 0, $i + 1)); } } // GET, POST, COOKIE $useFilter = (!in_array(ini_get('filter.default'), array('', 'unsafe_raw')) || ini_get('filter.default_flags')); - parse_str($url->query, $query); + parse_str($url->getQuery(), $query); if (!$query) { $query = $useFilter ? filter_input_array(INPUT_GET, FILTER_UNSAFE_RAW) : (empty($_GET) ? array() : $_GET); } @@ -255,7 +255,21 @@ public function createHttpRequest() $method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']; } - return new Request($url, $query, $post, $files, $cookies, $headers, $method, $remoteAddr, $remoteHost); + // raw body + $rawBodyCallback = function() { + static $rawBody; + + if (PHP_VERSION_ID >= 50600) { + return file_get_contents('php://input'); + + } elseif ($rawBody === NULL) { // can be read only once in PHP < 5.6 + $rawBody = (string) file_get_contents('php://input'); + } + + return $rawBody; + }; + + return new Request($url, $query, $post, $files, $cookies, $headers, $method, $remoteAddr, $remoteHost, $rawBodyCallback); } } diff --git a/src/Http/Response.php b/src/Http/Response.php index e7a3cf5f..8695c43a 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -47,10 +47,10 @@ class Response extends Nette\Object implements IResponse public function __construct() { if (PHP_VERSION_ID >= 50400) { - if (is_int(http_response_code())) { - $this->code = http_response_code(); + if (is_int($code = http_response_code())) { + $this->code = $code; } - header_register_callback($this->removeDuplicateCookies); + header_register_callback(array($this, 'removeDuplicateCookies')); } } @@ -147,13 +147,14 @@ public function redirect($url, $code = self::S302_FOUND) { $this->setCode($code); $this->setHeader('Location', $url); - echo "

Redirect

\n\n

Please click here to continue.

"; + $escapedUrl = htmlSpecialChars($url, ENT_IGNORE | ENT_QUOTES); + echo "

Redirect

\n\n

Please click here to continue.

"; } /** * Sets the number of seconds before a page cached on a browser expires. - * @param string|int|DateTime time, value 0 means "until the browser is closed" + * @param string|int|\DateTime time, value 0 means "until the browser is closed" * @return self * @throws Nette\InvalidStateException if HTTP headers have been sent */ @@ -167,7 +168,7 @@ public function setExpiration($time) $time = DateTime::from($time); $this->setHeader('Cache-Control', 'max-age=' . ($time->format('U') - time())); - $this->setHeader('Expires', self::date($time)); + $this->setHeader('Expires', Helpers::formatDate($time)); return $this; } @@ -183,7 +184,7 @@ public function isSent() /** - * Return the value of the HTTP header. + * Returns value of an HTTP header. * @param string * @param mixed * @return mixed @@ -203,7 +204,7 @@ public function getHeader($header, $default = NULL) /** * Returns a list of headers to sent. - * @return array + * @return array (name => value) */ public function getHeaders() { @@ -217,15 +218,11 @@ public function getHeaders() /** - * Returns HTTP valid date format. - * @param string|int|DateTime - * @return string + * @deprecated */ public static function date($time = NULL) { - $time = DateTime::from($time); - $time->setTimezone(new \DateTimeZone('GMT')); - return $time->format('D, d M Y H:i:s \G\M\T'); + return Helpers::formatDate($time); } @@ -248,7 +245,7 @@ public function __destruct() * Sends a cookie. * @param string name of the cookie * @param string value - * @param string|int|DateTime expiration time, value 0 means "until the browser is closed" + * @param string|int|\DateTime expiration time, value 0 means "until the browser is closed" * @param string * @param string * @param bool @@ -316,6 +313,7 @@ private function checkHeaders() { if (headers_sent($file, $line)) { throw new Nette\InvalidStateException('Cannot send header after HTTP headers have been sent' . ($file ? " (output started at $file:$line)." : '.')); + } elseif ($this->warnOnBuffer && ob_get_length() && !array_filter(ob_get_status(TRUE), function($i) { return !$i['chunk_size']; })) { trigger_error('Possible problem: you are sending a HTTP header while already having some data in output buffer. Try Tracy\OutputDebugger or start session earlier.', E_USER_NOTICE); } diff --git a/src/Http/Session.php b/src/Http/Session.php index 36943190..25b991c0 100644 --- a/src/Http/Session.php +++ b/src/Http/Session.php @@ -382,7 +382,7 @@ public function getOptions() /** - * Configurates session environment. + * Configures session environment. * @param array * @return void */ @@ -436,7 +436,7 @@ private function configure(array $config) /** * Sets the amount of time allowed between requests before the session will be terminated. - * @param string|int|DateTime time, value 0 means "until the browser is closed" + * @param string|int|\DateTime time, value 0 means "until the browser is closed" * @return self */ public function setExpiration($time) @@ -509,6 +509,7 @@ public function setStorage(ISessionStorage $storage) array($storage, 'open'), array($storage, 'close'), array($storage, 'read'), array($storage, 'write'), array($storage, 'remove'), array($storage, 'clean') ); + return $this; } @@ -522,6 +523,7 @@ public function setHandler(\SessionHandlerInterface $handler) throw new Nette\InvalidStateException('Unable to set handler when session has been started.'); } session_set_save_handler($handler); + return $this; } @@ -540,8 +542,8 @@ private function sendCookie() session_name(), session_id(), $cookie['lifetime'] ? $cookie['lifetime'] + time() : 0, $cookie['path'], $cookie['domain'], $cookie['secure'], $cookie['httponly'] - - )->setCookie( + ); + $this->response->setCookie( 'nette-browser', $_SESSION['__NF']['B'], Response::BROWSER, $cookie['path'], $cookie['domain'] ); diff --git a/src/Http/SessionSection.php b/src/Http/SessionSection.php index 3a8fe538..2c987d62 100644 --- a/src/Http/SessionSection.php +++ b/src/Http/SessionSection.php @@ -39,7 +39,7 @@ class SessionSection extends Nette\Object implements \IteratorAggregate, \ArrayA public function __construct(Session $session, $name) { if (!is_string($name)) { - throw new Nette\InvalidArgumentException("Session namespace must be a string, " . gettype($name) ." given."); + throw new Nette\InvalidArgumentException("Session namespace must be a string, " . gettype($name) . " given."); } $this->session = $session; @@ -177,7 +177,7 @@ public function offsetUnset($name) /** * Sets the expiration of the section or specific variables. - * @param string|int|DateTime time, value 0 means "until the browser is closed" + * @param string|int|\DateTime time, value 0 means "until the browser is closed" * @param mixed optional list of variables / single variable to expire * @return self */ diff --git a/src/Http/Url.php b/src/Http/Url.php index 6b0840a8..3d58e975 100644 --- a/src/Http/Url.php +++ b/src/Http/Url.php @@ -83,8 +83,8 @@ class Url extends Nette\Object /** - * @param string URL - * @throws Nette\InvalidArgumentException + * @param string|self + * @throws Nette\InvalidArgumentException if URL is malformed */ public function __construct($url = NULL) { @@ -260,7 +260,7 @@ public function setQuery($value) /** * Appends the query part of URI. * @param string|array - * @return Url + * @return self */ public function appendQuery($value) { @@ -405,8 +405,8 @@ public function getRelativeUrl() /** - * URI comparsion. - * @param string + * URL comparison. + * @param string|self * @return bool */ public function isEqual($url) @@ -429,8 +429,8 @@ public function isEqual($url) /** - * Transform to canonical form. - * @return Url + * Transforms URL to canonical form. + * @return self */ public function canonicalize() { diff --git a/src/Http/UserStorage.php b/src/Http/UserStorage.php index bce9865a..9b40dbd9 100644 --- a/src/Http/UserStorage.php +++ b/src/Http/UserStorage.php @@ -120,7 +120,7 @@ public function getNamespace() /** * Enables log out after inactivity. - * @param string|int|DateTime Number of seconds or timestamp + * @param string|int|\DateTime Number of seconds or timestamp * @param int Log out when the browser is closed | Clear the identity from persistent storage? * @return self */ @@ -147,7 +147,7 @@ public function setExpiration($time, $flags = 0) /** * Why was user logged out? - * @return int + * @return int|NULL */ public function getLogoutReason() { diff --git a/tests/Http/Helpers.phpt b/tests/Http/Helpers.phpt index fa2dfdd5..ee92862f 100644 --- a/tests/Http/Helpers.phpt +++ b/tests/Http/Helpers.phpt @@ -34,3 +34,12 @@ test(function() { Assert::false( Helpers::ipMatch('2001:db8:0:0:0:0:0:0', '2001:db8::/129') ); Assert::false( Helpers::ipMatch('2001:db8:0:0:0:0:0:0', '32.1.13.184/32') ); }); + + + +test(function() { + Assert::same( 'Tue, 15 Nov 1994 08:12:31 GMT', Helpers::formatDate('1994-11-15T08:12:31+0000') ); + Assert::same( 'Tue, 15 Nov 1994 08:12:31 GMT', Helpers::formatDate('1994-11-15T10:12:31+0200') ); + Assert::same( 'Tue, 15 Nov 1994 08:12:31 GMT', Helpers::formatDate(new DateTime('1994-11-15T06:12:31-0200')) ); + Assert::same( 'Tue, 15 Nov 1994 08:12:31 GMT', Helpers::formatDate(784887151) ); +}); diff --git a/tests/Http/Request.detectLanguage.phpt b/tests/Http/Request.detectLanguage.phpt new file mode 100644 index 00000000..d7d5a100 --- /dev/null +++ b/tests/Http/Request.detectLanguage.phpt @@ -0,0 +1,46 @@ + 'en, cs'); + $request = new Http\Request(new Http\UrlScript, NULL, NULL, NULL, NULL, $headers); + + Assert::same( 'en', $request->detectLanguage(array('en', 'cs')) ); + Assert::same( 'en', $request->detectLanguage(array('cs', 'en')) ); + Assert::null( $request->detectLanguage(array('xx')) ); +}); + + +test(function() { + $headers = array('Accept-Language' => 'da, en-gb;q=0.8, en;q=0.7'); + $request = new Http\Request(new Http\UrlScript, NULL, NULL, NULL, NULL, $headers); + + Assert::same( 'en-gb', $request->detectLanguage(array('en', 'en-gb')) ); + Assert::same( 'en', $request->detectLanguage(array('en')) ); +}); + + +test(function() { + $headers = array(); + $request = new Http\Request(new Http\UrlScript, NULL, NULL, NULL, NULL, $headers); + + Assert::null( $request->detectLanguage(array('en')) ); +}); + + +test(function() { + $headers = array('Accept-Language' => 'garbage'); + $request = new Http\Request(new Http\UrlScript, NULL, NULL, NULL, NULL, $headers); + + Assert::null( $request->detectLanguage(array('en')) ); +}); diff --git a/tests/Http/Request.getRawBody.phpt b/tests/Http/Request.getRawBody.phpt new file mode 100644 index 00000000..232cfe14 --- /dev/null +++ b/tests/Http/Request.getRawBody.phpt @@ -0,0 +1,27 @@ +getRawBody() ); +}); + + +test(function() { + $request = new Http\Request(new Http\UrlScript); + + Assert::null( $request->getRawBody() ); +});