diff --git a/.travis.yml b/.travis.yml index 98f53e5ca2..4cafa1a4b5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ env: global: - COMPOSER_ARGS="--no-interaction" - COVERAGE_DEPS="php-coveralls/php-coveralls" + - PHPSTAN_DEPS="phpstan/phpstan:^0.12" - TESTS_ZEND_HTTP_CLIENT_ONLINE=true - TESTS_ZEND_HTTP_CLIENT_BASEURI=http://127.0.0.1 - TESTS_ZEND_HTTP_CLIENT_HTTP_PROXY=127.0.0.1:8081 @@ -43,6 +44,7 @@ matrix: - DEPS=locked - CS_CHECK=true - TEST_COVERAGE=true + - PHPSTAN_TEST=true - php: 7.1 env: - DEPS=latest @@ -74,6 +76,7 @@ install: - if [[ $DEPS == 'latest' ]]; then travis_retry composer update $COMPOSER_ARGS ; fi - if [[ $DEPS == 'lowest' ]]; then travis_retry composer update --prefer-lowest --prefer-stable $COMPOSER_ARGS ; fi - if [[ $TEST_COVERAGE == 'true' ]]; then travis_retry composer require --dev $COMPOSER_ARGS $COVERAGE_DEPS ; fi + - if [[ $PHPSTAN_TEST == 'true' ]]; then travis_retry composer require --dev $COMPOSER_ARGS $PHPSTAN_DEPS ; fi - stty cols 120 && composer show before_script: @@ -105,6 +108,7 @@ before_script: script: - if [[ $TEST_COVERAGE == 'true' ]]; then composer test-coverage ; else composer test ; fi - if [[ $CS_CHECK == 'true' ]]; then composer cs-check ; fi + - if [[ $PHPSTAN_TEST == 'true' ]]; then ./vendor/bin/phpstan analyse --no-progress . ; fi after_script: - if [[ $TEST_COVERAGE == 'true' ]]; then travis_retry php vendor/bin/php-coveralls -v ; fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a5067ee22..ad83663308 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,10 @@ All notable changes to this project will be documented in this file, in reverse ### Changed -- Nothing. +- `Zend\Http\Request`'s URI now always have a default path ´/´ even when a `Zend\Uri\Http` + object is provided with no path. +- Relative URIs are not allowed anymore in `Zend\Http\Client`. + Anyway, usage of relative URIs has no sense and no adapter can handle it. ### Deprecated @@ -22,7 +25,9 @@ All notable changes to this project will be documented in this file, in reverse ### Fixed -- Nothing. +- Fixed `Zend\Http\Cookies::getAllCookies(Zend\Http\Cookies::COOKIE_STRING_ARRAY)`. +- Fixed `Zend\Http\Cookies::getAllCookies(Zend\Http\Cookies::COOKIE_STRING_CONCAT)`. +- Fixed use of HTTP URIs with empty path in Request ## 2.11.1 - TBD diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000000..53728fdd73 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,46 @@ +parameters: + level: max + + excludes_analyse: + - %currentWorkingDirectory%/docs/* + - %currentWorkingDirectory%/test/* + - %currentWorkingDirectory%/vendor/* + + universalObjectCratesClasses: + - Zend\Http\Header\Accept\FieldValuePart\AbstractFieldValuePart + + ignoreErrors: + - + message: '#Else branch is unreachable because ternary operator condition is always true#' + path: %currentWorkingDirectory%/src/Response.php + - + message: '#Result of && is always false#' + path: %currentWorkingDirectory%/src/PhpEnvironment/Request.php + - + message: '#Parameter \#1 $port of method Zend\\Uri\\Uri::setPort\(\) expects int, int|null given#' + path: %currentWorkingDirectory%/src/PhpEnvironment/Request.php + - + message: '#Result of && is always false#' + path: %currentWorkingDirectory%/src/Headers.php + - + message: '#Parameter \#1 \$fragment of method Zend\\Uri\\UriInterface::setFragment\(\) expects string, null given#' + path: %currentWorkingDirectory%/src/Header/Referer.php + - + message: '#Strict comparison using === between DateTime|string and 0 will always evaluate to false#' + path: %currentWorkingDirectory%/src/Header/Expires.php + - + message: '#Return type \(int\) of method Zend\\Http\\Header\\Age::getFieldValue\(\) should be compatible with return type \(string\) of method Zend\\Http\\Header\\HeaderInterface::getFieldValue\(\)#' + path: %currentWorkingDirectory%/src/Header/Age.php + - + message: '#Result of && is always false#' + path: %currentWorkingDirectory%/src/Header/Age.php + - + message: '#Access to an undefined property object::\$params#' + path: %currentWorkingDirectory%/src/Header/AbstractAccept.php + - + message: '#Negated boolean expression is always false#' + path: %currentWorkingDirectory%/src/Client/Adapter/Curl.php + - + message: '#Else branch is unreachable because previous condition is always true#' + path: %currentWorkingDirectory%/src/Client.php + - '#Access to an undefined property object::\$.+#' diff --git a/src/AbstractMessage.php b/src/AbstractMessage.php index 3ee1d51adc..73b79df8f7 100644 --- a/src/AbstractMessage.php +++ b/src/AbstractMessage.php @@ -30,7 +30,7 @@ abstract class AbstractMessage extends Message protected $version = self::VERSION_11; /** - * @var Headers|null + * @var Headers|string|null */ protected $headers; diff --git a/src/Client.php b/src/Client.php index 7626f7756e..5a0b43a0cd 100644 --- a/src/Client.php +++ b/src/Client.php @@ -11,6 +11,11 @@ use Traversable; use Zend\Http\Client\Adapter\Curl; use Zend\Http\Client\Adapter\Socket; +use Zend\Http\Client\Adapter\StreamInterface; +use Zend\Http\Exception\InvalidArgumentException; +use Zend\Http\Exception\RuntimeException; +use Zend\Http\Exception\UnexpectedValueException; +use Zend\Http\Header\HeaderInterface; use Zend\Http\Header\SetCookie; use Zend\Stdlib; use Zend\Stdlib\ArrayUtils; @@ -45,17 +50,17 @@ class Client implements Stdlib\DispatchableInterface const DIGEST_CNONCE = 'cnonce'; /** - * @var Response + * @var null|Response */ protected $response; /** - * @var Request + * @var null|Request */ protected $request; /** - * @var Client\Adapter\AdapterInterface + * @var null|Client\Adapter\AdapterInterface */ protected $adapter; @@ -65,14 +70,14 @@ class Client implements Stdlib\DispatchableInterface protected $auth = []; /** - * @var string + * @var null|string|resource */ protected $streamName; /** * @var resource|null */ - protected $streamHandle = null; + protected $streamHandle; /** * @var array of Header\SetCookie @@ -85,12 +90,12 @@ class Client implements Stdlib\DispatchableInterface protected $encType = ''; /** - * @var Request + * @var null|string */ protected $lastRawRequest; /** - * @var Response + * @var null|string */ protected $lastRawResponse; @@ -128,15 +133,15 @@ class Client implements Stdlib\DispatchableInterface * This variable is populated the first time _detectFileMimeType is called * and is then reused on every call to this method * - * @var resource + * @var null|resource */ protected static $fileInfoDb; /** * Constructor * - * @param string $uri - * @param array|Traversable $options + * @param null|string $uri + * @param null|array|Traversable $options */ public function __construct($uri = null, $options = null) { @@ -170,9 +175,7 @@ public function setOptions($options = []) } // Pass configuration options to the adapter if it exists - if ($this->adapter instanceof Client\Adapter\AdapterInterface) { - $this->adapter->setOptions($options); - } + $this->getAdapter()->setOptions($options); return $this; } @@ -203,9 +206,12 @@ public function setAdapter($adapter) } $this->adapter = $adapter; + $config = $this->config; unset($config['adapter']); - $this->adapter->setOptions($config); + + $adapter->setOptions($config); + return $this; } @@ -220,7 +226,10 @@ public function getAdapter() $this->setAdapter($this->config['adapter']); } - return $this->adapter; + /** @var Client\Adapter\AdapterInterface $adapter */ + $adapter = $this->adapter; + + return $adapter; } /** @@ -277,7 +286,7 @@ public function getResponse() /** * Get the last request (as a string) * - * @return string + * @return null|string */ public function getLastRawRequest() { @@ -287,7 +296,7 @@ public function getLastRawRequest() /** * Get the last response (as a string) * - * @return string + * @return null|string */ public function getLastRawResponse() { @@ -314,13 +323,18 @@ public function setUri($uri) { if (! empty($uri)) { // remember host of last request - $lastHost = $this->getRequest()->getUri()->getHost(); + $lastHost = $this->getRequest()->getUri()->getHost() ?: ''; $this->getRequest()->setUri($uri); // if host changed, the HTTP authentication should be cleared for security // reasons, see #4215 for a discussion - currently authentication is also // cleared for peer subdomains due to technical limits $nextHost = $this->getRequest()->getUri()->getHost(); + + if (! $nextHost) { + throw new InvalidArgumentException('Relative URIs are not allowed'); + } + if (! preg_match('/' . preg_quote($lastHost, '/') . '$/i', $nextHost)) { $this->clearAuth(); } @@ -412,7 +426,7 @@ public function getArgSeparator() { $argSeparator = $this->config['argseparator']; if (empty($argSeparator)) { - $argSeparator = ini_get('arg_separator.output'); + $argSeparator = ini_get('arg_separator.output') ?: '&'; $this->setArgSeparator($argSeparator); } return $argSeparator; @@ -421,14 +435,14 @@ public function getArgSeparator() /** * Set the encoding type and the boundary (if any) * - * @param string $encType - * @param string $boundary + * @param null|string $encType + * @param null|string $boundary * @return $this */ public function setEncType($encType, $boundary = null) { if (null === $encType || empty($encType)) { - $this->encType = null; + $this->encType = ''; return $this; } @@ -490,10 +504,9 @@ public function setParameterGet(array $query) * Reset all the HTTP parameters (request, response, etc) * * @param bool $clearCookies Also clear all valid cookies? (defaults to false) - * @param bool $clearAuth Also clear http authentication? (defaults to true) * @return $this */ - public function resetParameters($clearCookies = false /*, $clearAuth = true */) + public function resetParameters($clearCookies = false) { $clearAuth = true; if (func_num_args() > 1) { @@ -503,7 +516,7 @@ public function resetParameters($clearCookies = false /*, $clearAuth = true */) $uri = $this->getUri(); $this->streamName = null; - $this->encType = null; + $this->encType = ''; $this->request = null; $this->response = null; $this->lastRawRequest = null; @@ -535,12 +548,12 @@ public function getCookies() /** * Get the cookie Id (name+domain+path) * - * @param Header\SetCookie|Header\Cookie $cookie + * @param Header\SetCookie $cookie * @return string|bool */ protected function getCookieId($cookie) { - if (($cookie instanceof Header\SetCookie) || ($cookie instanceof Header\Cookie)) { + if ($cookie instanceof Header\SetCookie) { return $cookie->getName() . $cookie->getDomain() . $cookie->getPath(); } return false; @@ -550,14 +563,14 @@ protected function getCookieId($cookie) * Add a cookie * * @param array|ArrayIterator|Header\SetCookie|string $cookie - * @param string $value - * @param string $expire - * @param string $path - * @param string $domain - * @param bool $secure - * @param bool $httponly - * @param string $maxAge - * @param string $version + * @param null|string $value + * @param null|string $expire + * @param null|string $path + * @param null|string $domain + * @param bool $secure + * @param bool $httponly + * @param null|int $maxAge + * @param null|int $version * @throws Exception\InvalidArgumentException * @return $this */ @@ -681,12 +694,16 @@ public function getHeader($name) { $headers = $this->getRequest()->getHeaders(); - if ($headers instanceof Headers) { - if ($headers->get($name)) { - return $headers->get($name)->getFieldValue(); - } + if (! $headers instanceof Headers) { + return false; } - return false; + + $header = $headers->get($name); + if (! $header instanceof HeaderInterface) { + return false; + } + + return $header->getFieldValue(); } /** @@ -703,7 +720,7 @@ public function setStream($streamfile = true) /** * Get status of streaming for received data - * @return bool|string + * @return bool|resource|string */ public function getStream() { @@ -729,17 +746,20 @@ protected function openTempStream() $this->streamName = tempnam( isset($this->config['streamtmpdir']) ? $this->config['streamtmpdir'] : sys_get_temp_dir(), Client::class - ); + ) ?: null; } + /** @var string streamName */ + $streamName = $this->streamName; + ErrorHandler::start(); - $fp = fopen($this->streamName, 'w+b'); + $fp = fopen($streamName, 'w+b'); $error = ErrorHandler::stop(); if (false === $fp) { if ($this->adapter instanceof Client\Adapter\AdapterInterface) { $this->adapter->close(); } - throw new Exception\RuntimeException(sprintf('Could not open temp file %s', $this->streamName), 0, $error); + throw new Exception\RuntimeException(sprintf('Could not open temp file %s', $streamName), 0, $error); } return $fp; @@ -792,7 +812,7 @@ public function clearAuth() * @param string $user * @param string $password * @param string $type - * @param array $digest + * @param string[] $digest * @param null|string $entityBody * @throws Exception\InvalidArgumentException * @return string|bool @@ -821,7 +841,7 @@ protected function calcAuthDigest($user, $password, $type = self::AUTH_BASIC, $d throw new Exception\InvalidArgumentException('The digest cannot be empty'); } foreach ($digest as $key => $value) { - if (! defined('self::DIGEST_' . strtoupper($key))) { + if (! defined('self::DIGEST_' . strtoupper((string) $key))) { throw new Exception\InvalidArgumentException(sprintf( 'Invalid or not supported digest authentication parameter: \'%s\'', $key @@ -838,6 +858,8 @@ protected function calcAuthDigest($user, $password, $type = self::AUTH_BASIC, $d ); } $ha2 = md5($this->getMethod() . ':' . $this->getUri()->getPath() . ':' . md5($entityBody)); + } else { + throw new InvalidArgumentException('Invalid DIGEST auth data'); } if (empty($digest['qop'])) { $response = md5($ha1 . ':' . $digest['nonce'] . ':' . $ha2); @@ -855,10 +877,15 @@ protected function calcAuthDigest($user, $password, $type = self::AUTH_BASIC, $d * * @param Stdlib\RequestInterface $request * @param Stdlib\ResponseInterface $response + * @throws UnexpectedValueException on $request not an instance of Zend\http\Request * @return Stdlib\ResponseInterface */ public function dispatch(Stdlib\RequestInterface $request, Stdlib\ResponseInterface $response = null) { + if (! $request instanceof Request) { + throw UnexpectedValueException::unexpectedType(Request::class, $request); + } + return $this->send($request); } @@ -893,7 +920,7 @@ public function send(Request $request = null) if (! empty($queryArray)) { $newUri = $uri->toString(); - $queryString = http_build_query($queryArray, null, $this->getArgSeparator()); + $queryString = http_build_query($queryArray, '', $this->getArgSeparator()); if ($this->config['rfc3986strict']) { $queryString = str_replace('+', '%20', $queryString); @@ -925,7 +952,11 @@ public function send(Request $request = null) // headers $headers = $this->prepareHeaders($body, $uri); - $secure = $uri->getScheme() == 'https'; + $secure = $uri->getScheme() === 'https'; + + if (null === $uri->getHost()) { + throw new InvalidArgumentException('Invalid URI in request'); + } // cookies $cookie = $this->prepareCookies($uri->getHost(), $uri->getPath(), $secure); @@ -933,15 +964,11 @@ public function send(Request $request = null) $headers['Cookie'] = $cookie->getFieldValue(); } - // check that adapter supports streaming before using it - if (is_resource($body) && ! ($adapter instanceof Client\Adapter\StreamInterface)) { - throw new Client\Exception\RuntimeException('Adapter does not support streaming'); - } - $this->streamHandle = null; // calling protected method to allow extending classes // to wrap the interaction with the adapter $response = $this->doRequest($uri, $method, $secure, $headers, $body); + /** @var null|resource $stream */ $stream = $this->streamHandle; $this->streamHandle = null; @@ -959,19 +986,36 @@ public function send(Request $request = null) } if ($this->config['outputstream']) { + if (! $adapter instanceof StreamInterface) { + throw new Client\Exception\RuntimeException('Adapter does not support streaming'); + } + if ($stream === null) { + // @todo: check if it's really used $stream = $this->getStream(); if (! is_resource($stream) && is_string($stream)) { - $stream = fopen($stream, 'r'); + $stream = fopen($stream, 'rb'); } } + + if (! is_resource($stream)) { + throw new UnexpectedValueException('Stream is not a resource'); + } + $streamMetaData = stream_get_meta_data($stream); if ($streamMetaData['seekable']) { rewind($stream); } + // cleanup the adapter $adapter->setOutputStream(null); $response = Response\Stream::fromStream($response, $stream); + + // streamName can just be a string at this point + if (! is_string($this->streamName)) { + throw new UnexpectedValueException('Unexpected value for streamName'); + } + $response->setStreamName($this->streamName); if (! is_string($this->config['outputstream'])) { // we used temp name, will need to clean up @@ -983,15 +1027,20 @@ public function send(Request $request = null) // Get the cookies from response (if any) $setCookies = $response->getCookie(); - if (! empty($setCookies)) { + if (! is_bool($setCookies) && ! empty($setCookies)) { $this->addCookie($setCookies); } + /** @var Headers $responseHeaders */ + $responseHeaders = $response->getHeaders(); + // If we got redirected, look for the Location header - if ($response->isRedirect() && ($response->getHeaders()->has('Location'))) { + if ($response->isRedirect() && ($responseHeaders->has('Location'))) { // Avoid problems with buggy servers that add whitespace at the // end of some headers - $location = trim($response->getHeaders()->get('Location')->getFieldValue()); + /** @var HeaderInterface $locationHeader */ + $locationHeader = $responseHeaders->get('Location'); + $location = trim($locationHeader->getFieldValue()); // Check whether we send the exact same request again, or drop the parameters // and send a GET request @@ -1024,8 +1073,9 @@ public function send(Request $request = null) // Else, assume we have a relative path } else { // Get the current path directory, removing any trailing slashes - $path = $this->getUri()->getPath(); - $path = rtrim(substr($path, 0, strrpos($path, '/')), '/'); + $path = $this->getUri()->getPath() ?: '/'; + $slashPosition = strrpos($path, '/') ?: 0; + $path = rtrim(substr($path, 0, $slashPosition) ?: '', '/'); $this->getUri()->setPath($path . '/' . $location); } } @@ -1120,10 +1170,10 @@ public function removeFileUpload($filename) /** * Prepare Cookies * - * @param string $domain - * @param string $path + * @param null|string $domain + * @param null|string $path * @param bool $secure - * @return Header\Cookie|bool + * @return Header\Cookie */ protected function prepareCookies($domain, $path, $secure) { @@ -1161,6 +1211,11 @@ protected function prepareHeaders($body, $uri) { $headers = []; + $adapter = $this->getAdapter(); + + /** @var Headers|HeaderInterface[] $requestHeaders */ + $requestHeaders = $this->getRequest()->getHeaders(); + // Set the host header if ($this->config['httpversion'] == Request::VERSION_11) { $host = $uri->getHost(); @@ -1175,7 +1230,7 @@ protected function prepareHeaders($body, $uri) } // Set the connection header - if (! $this->getRequest()->getHeaders()->has('Connection')) { + if (! $requestHeaders->has('Connection')) { if (! $this->config['keepalive']) { $headers['Connection'] = 'close'; } @@ -1183,7 +1238,7 @@ protected function prepareHeaders($body, $uri) // Set the Accept-encoding header if not set - depending on whether // zlib is available or not. - if (! $this->getRequest()->getHeaders()->has('Accept-Encoding')) { + if (! $requestHeaders->has('Accept-Encoding')) { if (empty($this->config['outputstream']) && function_exists('gzinflate')) { $headers['Accept-Encoding'] = 'gzip, deflate'; } else { @@ -1192,7 +1247,7 @@ protected function prepareHeaders($body, $uri) } // Set the user agent header - if (! $this->getRequest()->getHeaders()->has('User-Agent') && isset($this->config['useragent'])) { + if (! $requestHeaders->has('User-Agent') && isset($this->config['useragent'])) { $headers['User-Agent'] = $this->config['useragent']; } @@ -1206,15 +1261,15 @@ protected function prepareHeaders($body, $uri) } break; case self::AUTH_DIGEST: - if (! $this->adapter instanceof Client\Adapter\Curl) { + if (! $adapter instanceof Client\Adapter\Curl) { throw new Exception\RuntimeException(sprintf( 'The digest authentication is only available for curl adapters (%s)', Curl::class )); } - $this->adapter->setCurlOption(CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); - $this->adapter->setCurlOption(CURLOPT_USERPWD, $this->auth['user'] . ':' . $this->auth['password']); + $adapter->setCurlOption(CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); + $adapter->setCurlOption(CURLOPT_USERPWD, $this->auth['user'] . ':' . $this->auth['password']); } } @@ -1235,7 +1290,6 @@ protected function prepareHeaders($body, $uri) // Merge the headers of the request (if any) // here we need right 'http field' and not lowercase letters - $requestHeaders = $this->getRequest()->getHeaders(); foreach ($requestHeaders as $requestHeaderElement) { $headers[$requestHeaderElement->getFieldName()] = $requestHeaderElement->getFieldValue(); } @@ -1263,14 +1317,18 @@ protected function prepareBody() $body = ''; $hasFiles = false; - if (! $this->getRequest()->getHeaders()->has('Content-Type')) { + /** @var Headers $headers */ + $headers = $this->getRequest()->getHeaders(); + + if (! $headers->has('Content-Type')) { $hasFiles = ! empty($this->getRequest()->getFiles()->toArray()); // If we have files to upload, force encType to multipart/form-data if ($hasFiles) { $this->setEncType(self::ENC_FORMDATA); } } else { - $this->setEncType($this->getHeader('Content-Type')); + $contentType = $this->getHeader('Content-Type'); + $this->setEncType(is_string($contentType) ? $contentType : ''); } // If we have POST parameters or files, encode and add them to the body @@ -1299,7 +1357,7 @@ protected function prepareBody() $body .= '--' . $boundary . '--' . "\r\n"; } elseif (stripos($this->getEncType(), self::ENC_URLENCODED) === 0) { // Encode body as application/x-www-form-urlencoded - $body = http_build_query($this->getRequest()->getPost()->toArray(), null, '&'); + $body = http_build_query($this->getRequest()->getPost()->toArray(), '', '&'); } else { throw new Client\Exception\RuntimeException(sprintf( 'Cannot handle content type \'%s\' automatically', @@ -1332,14 +1390,16 @@ protected function detectFileMimeType($file) // First try with fileinfo functions if (function_exists('finfo_open')) { + $fileInfoDb = static::$fileInfoDb; if (static::$fileInfoDb === null) { ErrorHandler::start(); - static::$fileInfoDb = finfo_open(FILEINFO_MIME); + $fileInfoDb = finfo_open(FILEINFO_MIME); ErrorHandler::stop(); } - if (static::$fileInfoDb) { - $type = finfo_file(static::$fileInfoDb, $file); + if ($fileInfoDb) { + static::$fileInfoDb = $fileInfoDb; + $type = finfo_file($fileInfoDb, $file); } } elseif (function_exists('mime_content_type')) { $type = mime_content_type($file); @@ -1413,7 +1473,7 @@ protected function flattenParametersArray($parray, $prefix = null) $key = $prefix . sprintf('[%s]', $name); } } else { - $key = $name; + $key = (string) $name; } if (is_array($value)) { @@ -1440,19 +1500,25 @@ protected function flattenParametersArray($parray, $prefix = null) */ protected function doRequest(Http $uri, $method, $secure = false, $headers = [], $body = '') { + if (null === $uri->getHost()) { + throw new InvalidArgumentException('URI does not have an host'); + } + + $adapter = $this->getAdapter(); + // Open the connection, send the request and read the response - $this->adapter->connect($uri->getHost(), $uri->getPort(), $secure); + $adapter->connect($uri->getHost(), $uri->getPort(), $secure); if ($this->config['outputstream']) { - if ($this->adapter instanceof Client\Adapter\StreamInterface) { + if ($adapter instanceof Client\Adapter\StreamInterface) { $this->streamHandle = $this->openTempStream(); - $this->adapter->setOutputStream($this->streamHandle); + $adapter->setOutputStream($this->streamHandle); } else { throw new Exception\RuntimeException('Adapter does not support streaming'); } } // HTTP connection - $this->lastRawRequest = $this->adapter->write( + $this->lastRawRequest = $adapter->write( $method, $uri, $this->config['httpversion'], @@ -1460,7 +1526,7 @@ protected function doRequest(Http $uri, $method, $secure = false, $headers = [], $body ); - return $this->adapter->read(); + return $adapter->read(); } /** diff --git a/src/Client/Adapter/Curl.php b/src/Client/Adapter/Curl.php index ff0e7cae1b..7d331cfa4f 100644 --- a/src/Client/Adapter/Curl.php +++ b/src/Client/Adapter/Curl.php @@ -56,14 +56,14 @@ class Curl implements HttpAdapter, StreamInterface /** * Response gotten from server * - * @var string + * @var null|string */ protected $response; /** * Stream for storing output * - * @var resource + * @var null|resource */ protected $outputStream; @@ -133,7 +133,7 @@ public function setOptions($options = []) } foreach ($options as $k => $v) { - $option = strtolower($k); + $option = strtolower((string) $k); switch ($option) { case 'proxyhost': $this->setCurlOption(CURLOPT_PROXY, $v); @@ -196,9 +196,10 @@ public function connect($host, $port = 80, $secure = false) } // Do the actual connection - $this->curl = curl_init(); + $curl = curl_init(); + if ($port != 80) { - curl_setopt($this->curl, CURLOPT_PORT, intval($port)); + curl_setopt($curl, CURLOPT_PORT, (int) $port); } if (isset($this->config['connecttimeout'])) { @@ -222,33 +223,33 @@ public function connect($host, $port = 80, $secure = false) if ($connectTimeout !== null) { if (defined('CURLOPT_CONNECTTIMEOUT_MS')) { - curl_setopt($this->curl, CURLOPT_CONNECTTIMEOUT_MS, $connectTimeout * 1000); + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT_MS, $connectTimeout * 1000); } else { - curl_setopt($this->curl, CURLOPT_CONNECTTIMEOUT, $connectTimeout); + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $connectTimeout); } } if (isset($this->config['timeout'])) { if (defined('CURLOPT_TIMEOUT_MS')) { - curl_setopt($this->curl, CURLOPT_TIMEOUT_MS, $this->config['timeout'] * 1000); + curl_setopt($curl, CURLOPT_TIMEOUT_MS, $this->config['timeout'] * 1000); } else { - curl_setopt($this->curl, CURLOPT_TIMEOUT, $this->config['timeout']); + curl_setopt($curl, CURLOPT_TIMEOUT, $this->config['timeout']); } } if (isset($this->config['sslcafile']) && $this->config['sslcafile']) { - curl_setopt($this->curl, CURLOPT_CAINFO, $this->config['sslcafile']); + curl_setopt($curl, CURLOPT_CAINFO, $this->config['sslcafile']); } if (isset($this->config['sslcapath']) && $this->config['sslcapath']) { - curl_setopt($this->curl, CURLOPT_CAPATH, $this->config['sslcapath']); + curl_setopt($curl, CURLOPT_CAPATH, $this->config['sslcapath']); } if (isset($this->config['maxredirects'])) { // Set Max redirects - curl_setopt($this->curl, CURLOPT_MAXREDIRS, $this->config['maxredirects']); + curl_setopt($curl, CURLOPT_MAXREDIRS, $this->config['maxredirects']); } - if (! $this->curl) { + if (! $curl) { $this->close(); throw new AdapterException\RuntimeException('Unable to Connect to ' . $host . ':' . $port); @@ -257,13 +258,15 @@ public function connect($host, $port = 80, $secure = false) if ($secure !== false) { // Behave the same like Zend\Http\Adapter\Socket on SSL options. if (isset($this->config['sslcert'])) { - curl_setopt($this->curl, CURLOPT_SSLCERT, $this->config['sslcert']); + curl_setopt($curl, CURLOPT_SSLCERT, $this->config['sslcert']); } if (isset($this->config['sslpassphrase'])) { - curl_setopt($this->curl, CURLOPT_SSLCERTPASSWD, $this->config['sslpassphrase']); + curl_setopt($curl, CURLOPT_SSLCERTPASSWD, $this->config['sslpassphrase']); } } + $this->curl = $curl; + // Update connected_to $this->connectedTo = [$host, $port]; } @@ -271,20 +274,22 @@ public function connect($host, $port = 80, $secure = false) /** * Send request to the remote server * - * @param string $method - * @param \Zend\Uri\Uri $uri - * @param float $httpVersion - * @param array $headers - * @param string $body - * @return string $request + * @param string $method + * @param \Zend\Uri\Uri $uri + * @param string $httpVersion + * @param array $headers + * @param string|resource $body + * @return string $request * @throws AdapterException\RuntimeException If connection fails, connected * to wrong host, no PUT file defined, unsupported method, or unsupported * cURL option. * @throws AdapterException\InvalidArgumentException if $method is currently not supported * @throws AdapterException\TimeoutException if connection timed out */ - public function write($method, $uri, $httpVersion = 1.1, $headers = [], $body = '') + public function write($method, $uri, $httpVersion = '1.1', $headers = [], $body = '') { + $httpVersion = (string) $httpVersion; + // Make sure we're properly connected if (! $this->curl) { throw new AdapterException\RuntimeException('Trying to write but we are not connected'); @@ -380,7 +385,7 @@ public function write($method, $uri, $httpVersion = 1.1, $headers = [], $body = } // get http version to use - $curlHttp = $httpVersion == 1.1 ? CURL_HTTP_VERSION_1_1 : CURL_HTTP_VERSION_1_0; + $curlHttp = $httpVersion === '1.1' ? CURL_HTTP_VERSION_1_1 : CURL_HTTP_VERSION_1_0; // mark as HTTP request and set HTTP method curl_setopt($this->curl, CURLOPT_HTTP_VERSION, $curlHttp); @@ -455,16 +460,11 @@ public function write($method, $uri, $httpVersion = 1.1, $headers = [], $body = // send the request + /** @var false|string $response */ $response = curl_exec($this->curl); - // if we used streaming, headers are already there - if (! is_resource($this->outputStream)) { - $this->response = $response; - } - - $request = curl_getinfo($this->curl, CURLINFO_HEADER_OUT); - $request .= $body; if ($response === false || empty($this->response)) { + $this->response = null; if (curl_errno($this->curl) === static::ERROR_OPERATION_TIMEDOUT) { throw new AdapterException\TimeoutException( 'Read timed out', @@ -477,18 +477,28 @@ public function write($method, $uri, $httpVersion = 1.1, $headers = [], $body = )); } + // if we used streaming, headers are already there + if (! is_resource($this->outputStream)) { + $this->response = $response; + } + + $request = curl_getinfo($this->curl, CURLINFO_HEADER_OUT); + $request .= $body; + // separating header from body because it is dangerous to accidentially replace strings in the body $responseHeaderSize = curl_getinfo($this->curl, CURLINFO_HEADER_SIZE); $responseHeaders = substr($this->response, 0, $responseHeaderSize); // cURL automatically decodes chunked-messages, this means we have to // disallow the Zend\Http\Response to do it again. + /** @var string $responseHeaders */ $responseHeaders = preg_replace("/Transfer-Encoding:\s*chunked\\r\\n/i", '', $responseHeaders); // cURL can automatically handle content encoding; prevent double-decoding from occurring if (isset($this->config['curloptions'][CURLOPT_ENCODING]) && '' == $this->config['curloptions'][CURLOPT_ENCODING] ) { + /** @var string $responseHeaders */ $responseHeaders = preg_replace("/Content-Encoding:\s*gzip\\r\\n/i", '', $responseHeaders); } @@ -505,9 +515,10 @@ public function write($method, $uri, $httpVersion = 1.1, $headers = [], $body = // Eliminate multiple HTTP responses. do { $parts = preg_split('|(?:\r?\n){2}|m', $this->response, 2); + $again = false; - if (isset($parts[1]) && preg_match("|^HTTP/1\.[01](.*?)\r\n|mi", $parts[1])) { + if (is_array($parts) && isset($parts[1]) && preg_match("|^HTTP/1\.[01](.*?)\r\n|mi", $parts[1])) { $this->response = $parts[1]; $again = true; } @@ -519,7 +530,7 @@ public function write($method, $uri, $httpVersion = 1.1, $headers = [], $body = /** * Return read response from server * - * @return string + * @return null|string */ public function read() { @@ -542,7 +553,7 @@ public function close() /** * Get cUrl Handle * - * @return resource + * @return null|resource */ public function getHandle() { @@ -552,7 +563,7 @@ public function getHandle() /** * Set output stream for the response * - * @param resource $stream + * @param null|resource $stream * @return $this */ public function setOutputStream($stream) diff --git a/src/Client/Adapter/Exception/UnexpectedValueException.php b/src/Client/Adapter/Exception/UnexpectedValueException.php new file mode 100644 index 0000000000..f4f3af3693 --- /dev/null +++ b/src/Client/Adapter/Exception/UnexpectedValueException.php @@ -0,0 +1,15 @@ +getHost(); + $scheme = $uri->getScheme(); + $port = $uri->getPort(); + + if (null === $host || null === $scheme || null === $port) { + throw new AdapterException\InvalidArgumentException( + 'Invalid Uri object' + ); + } + // if we are proxying HTTPS, preform CONNECT handshake with the proxy if ($isSecure && ! $this->negotiated) { - $this->connectHandshake($uri->getHost(), $uri->getPort(), $httpVer, $headers); + $this->connectHandshake($host, $port, $httpVer, $headers); $this->negotiated = true; } @@ -226,6 +236,10 @@ public function write($method, $uri, $httpVer = '1.1', $headers = [], $body = '' */ protected function connectHandshake($host, $port = 443, $httpVer = '1.1', array &$headers = []) { + if (null === $this->socket) { + throw new AdapterException\RuntimeException('Socket is not initialized'); + } + $request = 'CONNECT ' . $host . ':' . $port . ' HTTP/' . $httpVer . "\r\n" . 'Host: ' . $host . "\r\n"; diff --git a/src/Client/Adapter/Socket.php b/src/Client/Adapter/Socket.php index a443f40fdc..7cc35cf97f 100644 --- a/src/Client/Adapter/Socket.php +++ b/src/Client/Adapter/Socket.php @@ -10,6 +10,8 @@ use Traversable; use Zend\Http\Client\Adapter\AdapterInterface as HttpAdapter; use Zend\Http\Client\Adapter\Exception as AdapterException; +use Zend\Http\Header\HeaderInterface; +use Zend\Http\Headers; use Zend\Http\Request; use Zend\Http\Response; use Zend\Stdlib\ArrayUtils; @@ -50,7 +52,7 @@ class Socket implements HttpAdapter, StreamInterface /** * Stream for storing output * - * @var resource + * @var null|resource */ protected $outStream; @@ -82,7 +84,7 @@ class Socket implements HttpAdapter, StreamInterface /** * Stream context * - * @var resource + * @var null|resource */ protected $context; @@ -283,10 +285,10 @@ public function connect($host, $port = 80, $secure = false) (int) $connectTimeout, $flags, $context - ); + ) ?: null; $error = ErrorHandler::stop(); - if (! $this->socket) { + if (! is_resource($this->socket)) { $this->close(); throw new AdapterException\RuntimeException( sprintf( @@ -368,11 +370,11 @@ public function connect($host, $port = 80, $secure = false) /** * Send request to the remote server * - * @param string $method - * @param \Zend\Uri\Uri $uri - * @param string $httpVer - * @param array $headers - * @param string $body + * @param string $method + * @param \Zend\Uri\Uri $uri + * @param string $httpVer + * @param array $headers + * @param string|resource $body * @throws AdapterException\RuntimeException * @return string Request as string */ @@ -384,8 +386,17 @@ public function write($method, $uri, $httpVer = '1.1', $headers = [], $body = '' } $host = $uri->getHost(); - $host = (strtolower($uri->getScheme()) == 'https' ? $this->config['ssltransport'] : 'tcp') . '://' . $host; - if ($this->connectedTo[0] != $host || $this->connectedTo[1] != $uri->getPort()) { + $scheme = $uri->getScheme(); + $port = $uri->getPort(); + + if (null === $host || null === $scheme || null === $port) { + throw new AdapterException\InvalidArgumentException( + 'Invalid Uri object' + ); + } + + $host = (strtolower($scheme) == 'https' ? $this->config['ssltransport'] : 'tcp') . '://' . $host; + if ($this->connectedTo[0] != $host || $this->connectedTo[1] != $port) { throw new AdapterException\RuntimeException('Trying to write but we are connected to the wrong host'); } @@ -440,7 +451,12 @@ public function read() $response = ''; $gotStatus = false; - while (($line = fgets($this->socket)) !== false) { + if (null === $this->socket) { + throw new AdapterException\RuntimeException('Socket is not initialized'); + } + + $line = fgets($this->socket); + while (false !== $line) { $gotStatus = $gotStatus || (strpos($line, 'HTTP') !== false); if ($gotStatus) { $response .= $line; @@ -448,6 +464,8 @@ public function read() break; } } + + $line = fgets($this->socket); } $this->_checkSocketReadTimeout(); @@ -462,6 +480,7 @@ public function read() } // Check headers to see what kind of connection / transfer encoding we have + /** @var Headers $headers */ $headers = $responseObj->getHeaders(); /** @@ -473,6 +492,7 @@ public function read() || $this->method == Request::METHOD_HEAD ) { // Close the connection if requested to do so by the server + /** @var false|HeaderInterface $connection */ $connection = $headers->get('connection'); if ($connection && $connection->getFieldValue() == 'close') { $this->close(); @@ -481,7 +501,9 @@ public function read() } // If we got a 'transfer-encoding: chunked' header + /** @var false|HeaderInterface $transferEncoding */ $transferEncoding = $headers->get('transfer-encoding'); + /** @var false|HeaderInterface $contentLength */ $contentLength = $headers->get('content-length'); if ($transferEncoding !== false) { if (strtolower($transferEncoding->getFieldValue()) == 'chunked') { @@ -489,6 +511,10 @@ public function read() $line = fgets($this->socket); $this->_checkSocketReadTimeout(); + if (false === $line) { + throw new AdapterException\RuntimeException('Unable to read from socket resource'); + } + $chunk = $line; // Figure out the next chunk size @@ -502,7 +528,7 @@ public function read() } // Convert the hexadecimal value to plain integer - $chunksize = hexdec($chunksize); + $chunksize = (int) hexdec($chunksize); // Read next chunk $readTo = ftell($this->socket) + $chunksize; @@ -557,7 +583,7 @@ public function read() if (is_array($contentLength)) { $contentLength = $contentLength[count($contentLength) - 1]; } - $contentLength = $contentLength->getFieldValue(); + $contentLength = (int) $contentLength->getFieldValue(); $currentPos = ftell($this->socket); @@ -608,8 +634,9 @@ public function read() } // Close the connection if requested to do so by the server + /** @var false|HeaderInterface $connection */ $connection = $headers->get('connection'); - if ($connection && $connection->getFieldValue() == 'close') { + if ($connection && $connection->getFieldValue() === 'close') { $this->close(); } @@ -641,23 +668,29 @@ public function close() protected function _checkSocketReadTimeout() { // @codingStandardsIgnoreEnd - if ($this->socket) { - $info = stream_get_meta_data($this->socket); - $timedout = $info['timed_out']; - if ($timedout) { - $this->close(); - throw new AdapterException\TimeoutException( - sprintf('Read timed out after %d seconds', $this->config['timeout']), - AdapterException\TimeoutException::READ_TIMEOUT - ); - } + if (! $this->socket) { + return; } + + $info = stream_get_meta_data($this->socket); + $timedout = $info['timed_out']; + + if (! $timedout) { + return; + } + + $this->close(); + + throw new AdapterException\TimeoutException( + sprintf('Read timed out after %d seconds', $this->config['timeout']), + AdapterException\TimeoutException::READ_TIMEOUT + ); } /** * Set output stream for the response * - * @param resource $stream + * @param null|resource $stream * @return \Zend\Http\Client\Adapter\Socket */ public function setOutputStream($stream) diff --git a/src/Client/Adapter/StreamInterface.php b/src/Client/Adapter/StreamInterface.php index 808f324e19..8868d25f8d 100644 --- a/src/Client/Adapter/StreamInterface.php +++ b/src/Client/Adapter/StreamInterface.php @@ -19,7 +19,7 @@ interface StreamInterface * * This function sets output stream where the result will be stored. * - * @param resource $stream Stream to write the output to + * @param null|resource $stream Stream to write the output to * */ public function setOutputStream($stream); diff --git a/src/Client/Adapter/Test.php b/src/Client/Adapter/Test.php index d65bba191e..ff5298e2d0 100644 --- a/src/Client/Adapter/Test.php +++ b/src/Client/Adapter/Test.php @@ -61,7 +61,7 @@ public function __construct() * Set the nextRequestWillFail flag * * @param bool $flag - * @return \Zend\Http\Client\Adapter\Test + * @return self */ public function setNextRequestWillFail($flag) { diff --git a/src/Client/Exception/UnexpectedValueException.php b/src/Client/Exception/UnexpectedValueException.php new file mode 100644 index 0000000000..7e5472c7be --- /dev/null +++ b/src/Client/Exception/UnexpectedValueException.php @@ -0,0 +1,15 @@ +getHeaders()->addHeaders($headers); + /** @var Headers $requestHeaders */ + $requestHeaders = $request->getHeaders(); + $requestHeaders->addHeaders($headers); } if (! empty($body)) { @@ -98,7 +102,9 @@ public static function post($url, $params, $headers = [], $body = null, $clientO } if (! empty($headers) && is_array($headers)) { - $request->getHeaders()->addHeaders($headers); + /** @var Headers $requestHeaders */ + $requestHeaders = $request->getHeaders(); + $requestHeaders->addHeaders($headers); } if (! empty($body)) { diff --git a/src/Cookies.php b/src/Cookies.php index 0f44b8ef18..dad5ecc56b 100644 --- a/src/Cookies.php +++ b/src/Cookies.php @@ -8,6 +8,8 @@ namespace Zend\Http; use ArrayIterator; +use Zend\Http\Exception\InvalidArgumentException; +use Zend\Http\Exception\LogicException; use Zend\Http\Header\SetCookie; use Zend\Uri; @@ -71,7 +73,7 @@ class Cookies extends Headers /** * @static * @throws Exception\RuntimeException - * @param $string + * @param string $string * @return void */ public static function fromString($string) @@ -87,13 +89,13 @@ public static function fromString($string) * or as a string - in which case an object is created from the string. * * @param SetCookie|string $cookie - * @param Uri\Uri|string $refUri Optional reference URI (for domain, path, secure) + * @param null|Uri\Uri|string $refUri Optional reference URI (for domain, path, secure) * @throws Exception\InvalidArgumentException */ public function addCookie($cookie, $refUri = null) { if (is_string($cookie)) { - $cookie = SetCookie::fromString($cookie, $refUri); + $cookie = SetCookie::fromString($cookie); } if ($cookie instanceof SetCookie) { @@ -120,14 +122,16 @@ public function addCookie($cookie, $refUri = null) */ public function addCookiesFromResponse(Response $response, $refUri) { - $cookieHdrs = $response->getHeaders()->get('Set-Cookie'); + /** @var Headers $headers */ + $headers = $response->getHeaders(); + $cookieHeaders = $headers->get('Set-Cookie'); - if (is_array($cookieHdrs) || $cookieHdrs instanceof ArrayIterator) { - foreach ($cookieHdrs as $cookie) { + if ($cookieHeaders instanceof ArrayIterator) { + foreach ($cookieHeaders as $cookie) { $this->addCookie($cookie, $refUri); } - } elseif (is_string($cookieHdrs)) { - $this->addCookie($cookieHdrs, $refUri); + } elseif (is_string($cookieHeaders)) { + $this->addCookie($cookieHeaders, $refUri); } } @@ -197,7 +201,7 @@ public function getMatchingCookies( * @param string $cookieName The cookie's name * @param int $retAs Whether to return cookies as objects of \Zend\Http\Header\SetCookie or as strings * @throws Exception\InvalidArgumentException if invalid URI specified or invalid $retAs value - * @return SetCookie|string + * @return SetCookie|string|bool */ public function getCookie($uri, $cookieName, $retAs = self::COOKIE_OBJECT) { @@ -213,7 +217,7 @@ public function getCookie($uri, $cookieName, $retAs = self::COOKIE_OBJECT) } // Get correct cookie path - $path = $uri->getPath(); + $path = $uri->getPath() ?: '/'; $lastSlashPos = strrpos($path, '/') ?: 0; $path = substr($path, 0, $lastSlashPos); if (! $path) { @@ -221,6 +225,7 @@ public function getCookie($uri, $cookieName, $retAs = self::COOKIE_OBJECT) } if (isset($this->cookies[$uri->getHost()][$path][$cookieName])) { + /** @var SetCookie $cookie */ $cookie = $this->cookies[$uri->getHost()][$path][$cookieName]; switch ($retAs) { @@ -229,7 +234,7 @@ public function getCookie($uri, $cookieName, $retAs = self::COOKIE_OBJECT) case self::COOKIE_STRING_ARRAY: case self::COOKIE_STRING_CONCAT: - return $cookie->__toString(); + return $cookie->toString(); default: throw new Exception\InvalidArgumentException(sprintf( @@ -260,25 +265,30 @@ protected function _flattenCookiesArray($ptr, $retAs = self::COOKIE_OBJECT) if ($retAs == self::COOKIE_STRING_CONCAT) { $ret .= $this->_flattenCookiesArray($item, $retAs); } else { - $ret = array_merge($ret, $this->_flattenCookiesArray($item, $retAs)); + $flatten = $this->_flattenCookiesArray($item, $retAs); + if (! is_array($ret) || ! is_array($flatten)) { + throw new LogicException('Flatten cookies is not an array'); + } + $ret = array_merge($ret, $flatten); } } return $ret; - } elseif ($ptr instanceof SetCookie) { + } + + if ($ptr instanceof SetCookie) { switch ($retAs) { case self::COOKIE_STRING_ARRAY: - return [$ptr->__toString()]; + return [$ptr->toString()]; case self::COOKIE_STRING_CONCAT: - return $ptr->__toString(); + case self::COOKIE_STRING_CONCAT_STRICT: + return $ptr->toString(); case self::COOKIE_OBJECT: default: return [$ptr]; } } - - return; } /** @@ -305,7 +315,7 @@ protected function _matchDomain($domain) /** * Return a subset of a domain-matching cookies that also match a specified path * - * @param array $domains + * @param array> $domains * @param string $path * @return array */ @@ -316,7 +326,9 @@ protected function _matchPath($domains, $path) $ret = []; foreach ($domains as $dom => $pathsArray) { - foreach (array_keys($pathsArray) as $cpath) { + /** @var string[] $keys */ + $keys = array_keys($pathsArray); + foreach ($keys as $cpath) { if (SetCookie::matchCookiePath($cpath, $path)) { if (! isset($ret[$dom])) { $ret[$dom] = []; diff --git a/src/Exception/LogicException.php b/src/Exception/LogicException.php new file mode 100644 index 0000000000..48f9dbc4a0 --- /dev/null +++ b/src/Exception/LogicException.php @@ -0,0 +1,13 @@ +match($matchAgainst); + return false !== $this->match($matchAgainst); } /** * Match a media string against this header * * @param array|string $matchAgainst - * @return Accept\FieldValuePArt\AcceptFieldValuePart|bool The matched value or false + * @return Accept\FieldValuePart\AcceptFieldValuePart|bool The matched value or false */ public function match($matchAgainst) { @@ -330,9 +329,9 @@ public function match($matchAgainst) /** * Return a match where all parameters in argument #1 match those in argument #2 * - * @param array $match1 - * @param array $match2 - * @return bool|array + * @param object $match1 + * @param object $match2 + * @return bool|object */ protected function matchAcceptParams($match1, $match2) { @@ -376,8 +375,8 @@ protected function matchAcceptParams($match1, $match2) /** * Add a key/value combination to the internal queue * - * @param stdClass $value - * @return number + * @param object $value + * @return void */ protected function addFieldValuePartToQueue($value) { diff --git a/src/Header/AbstractDate.php b/src/Header/AbstractDate.php index 001a907cb8..d0d9cf23cc 100644 --- a/src/Header/AbstractDate.php +++ b/src/Header/AbstractDate.php @@ -95,7 +95,13 @@ public static function fromString($headerLine) */ public static function fromTimeString($time) { - return static::fromTimestamp(strtotime($time)); + $timestamp = strtotime((string) $time); + + if (false === $timestamp) { + throw new Exception\InvalidArgumentException(sprintf('Invalid time %s', $time)); + } + + return static::fromTimestamp($timestamp); } /** @@ -159,14 +165,16 @@ public function setDate($date) { if (is_string($date)) { try { - $date = new DateTime($date, new DateTimeZone('GMT')); + $dateObj = new DateTime($date, new DateTimeZone('GMT')); } catch (\Exception $e) { throw new Exception\InvalidArgumentException( - sprintf('Invalid date passed as string (%s)', (string) $date), + sprintf('Invalid date passed as string (%s)', $date), $e->getCode(), $e ); } + + $date = $dateObj; } elseif (! ($date instanceof DateTime)) { throw new Exception\InvalidArgumentException('Date must be an instance of \DateTime or a string'); } @@ -195,7 +203,7 @@ public function getDate() public function date() { if ($this->date === null) { - $this->date = new DateTime(null, new DateTimeZone('GMT')); + $this->date = new DateTime('now', new DateTimeZone('GMT')); } return $this->date; } @@ -213,14 +221,16 @@ public function compareTo($date) { if (is_string($date)) { try { - $date = new DateTime($date, new DateTimeZone('GMT')); + $dateObj = new DateTime($date, new DateTimeZone('GMT')); } catch (\Exception $e) { throw new Exception\InvalidArgumentException( - sprintf('Invalid Date passed as string (%s)', (string) $date), + sprintf('Invalid Date passed as string (%s)', $date), $e->getCode(), $e ); } + + $date = $dateObj; } elseif (! ($date instanceof DateTime)) { throw new Exception\InvalidArgumentException('Date must be an instance of \DateTime or a string'); } diff --git a/src/Header/Accept.php b/src/Header/Accept.php index 19d131dcaf..5c41231b0a 100644 --- a/src/Header/Accept.php +++ b/src/Header/Accept.php @@ -51,7 +51,9 @@ public function toString() */ public function addMediaType($type, $priority = 1, array $params = []) { - return $this->addType($type, $priority, $params); + $this->addType($type, $priority, $params); + + return $this; } /** diff --git a/src/Header/Accept/FieldValuePart/AbstractFieldValuePart.php b/src/Header/Accept/FieldValuePart/AbstractFieldValuePart.php index 95e2020cfc..bce07c447d 100644 --- a/src/Header/Accept/FieldValuePart/AbstractFieldValuePart.php +++ b/src/Header/Accept/FieldValuePart/AbstractFieldValuePart.php @@ -69,7 +69,7 @@ protected function getInternalValues() */ public function getTypeString() { - return $this->getInternalValues()->typeString; + return $this->typeString; } /** @@ -77,15 +77,15 @@ public function getTypeString() */ public function getPriority() { - return (float) $this->getInternalValues()->priority; + return (float) $this->priority; } /** - * @return \stdClass $params + * @return object $params */ public function getParams() { - return (object) $this->getInternalValues()->params; + return (object) $this->params; } /** @@ -93,7 +93,7 @@ public function getParams() */ public function getRaw() { - return $this->getInternalValues()->raw; + return $this->raw; } /** diff --git a/src/Header/Accept/FieldValuePart/AcceptFieldValuePart.php b/src/Header/Accept/FieldValuePart/AcceptFieldValuePart.php index 8631b64488..7955e8f442 100644 --- a/src/Header/Accept/FieldValuePart/AcceptFieldValuePart.php +++ b/src/Header/Accept/FieldValuePart/AcceptFieldValuePart.php @@ -19,7 +19,7 @@ class AcceptFieldValuePart extends AbstractFieldValuePart */ public function getSubtype() { - return $this->getInternalValues()->subtype; + return $this->subtype; } /** @@ -27,7 +27,7 @@ public function getSubtype() */ public function getSubtypeRaw() { - return $this->getInternalValues()->subtypeRaw; + return $this->subtypeRaw; } /** @@ -35,6 +35,6 @@ public function getSubtypeRaw() */ public function getFormat() { - return $this->getInternalValues()->format; + return $this->format; } } diff --git a/src/Header/Accept/FieldValuePart/CharsetFieldValuePart.php b/src/Header/Accept/FieldValuePart/CharsetFieldValuePart.php index e137c493ac..11e6c5d59f 100644 --- a/src/Header/Accept/FieldValuePart/CharsetFieldValuePart.php +++ b/src/Header/Accept/FieldValuePart/CharsetFieldValuePart.php @@ -19,6 +19,6 @@ class CharsetFieldValuePart extends AbstractFieldValuePart */ public function getCharset() { - return $this->getInternalValues()->type; + return $this->type; } } diff --git a/src/Header/Accept/FieldValuePart/EncodingFieldValuePart.php b/src/Header/Accept/FieldValuePart/EncodingFieldValuePart.php index de26751138..b5a2ac47ef 100644 --- a/src/Header/Accept/FieldValuePart/EncodingFieldValuePart.php +++ b/src/Header/Accept/FieldValuePart/EncodingFieldValuePart.php @@ -19,6 +19,6 @@ class EncodingFieldValuePart extends AbstractFieldValuePart */ public function getEncoding() { - return $this->getInternalValues()->type; + return $this->type; } } diff --git a/src/Header/Accept/FieldValuePart/LanguageFieldValuePart.php b/src/Header/Accept/FieldValuePart/LanguageFieldValuePart.php index 0ca4c0a0c7..77affe6211 100644 --- a/src/Header/Accept/FieldValuePart/LanguageFieldValuePart.php +++ b/src/Header/Accept/FieldValuePart/LanguageFieldValuePart.php @@ -16,16 +16,16 @@ class LanguageFieldValuePart extends AbstractFieldValuePart { public function getLanguage() { - return $this->getInternalValues()->typeString; + return $this->typeString; } public function getPrimaryTag() { - return $this->getInternalValues()->type; + return $this->type; } public function getSubTag() { - return $this->getInternalValues()->subtype; + return $this->subtype; } } diff --git a/src/Header/AcceptCharset.php b/src/Header/AcceptCharset.php index 8d196ec7a4..5994c7ca2d 100644 --- a/src/Header/AcceptCharset.php +++ b/src/Header/AcceptCharset.php @@ -47,7 +47,9 @@ public function toString() */ public function addCharset($type, $priority = 1) { - return $this->addType($type, $priority); + $this->addType($type, $priority); + + return $this; } /** diff --git a/src/Header/AcceptEncoding.php b/src/Header/AcceptEncoding.php index da1c960e47..61b552a2a7 100644 --- a/src/Header/AcceptEncoding.php +++ b/src/Header/AcceptEncoding.php @@ -47,7 +47,9 @@ public function toString() */ public function addEncoding($type, $priority = 1) { - return $this->addType($type, $priority); + $this->addType($type, $priority); + + return $this; } /** diff --git a/src/Header/AcceptLanguage.php b/src/Header/AcceptLanguage.php index c8a90b2544..9452df557e 100644 --- a/src/Header/AcceptLanguage.php +++ b/src/Header/AcceptLanguage.php @@ -47,7 +47,9 @@ public function toString() */ public function addLanguage($type, $priority = 1) { - return $this->addType($type, $priority); + $this->addType($type, $priority); + + return $this; } /** diff --git a/src/Header/CacheControl.php b/src/Header/CacheControl.php index 1aab3c009f..daae61d586 100644 --- a/src/Header/CacheControl.php +++ b/src/Header/CacheControl.php @@ -186,14 +186,13 @@ protected static function parseValue($value) switch (static::match(['[a-zA-Z][a-zA-Z_-]*'], $value, $lastMatch)) { case 0: $directive = $lastMatch; - goto state_value; + break; // intentional fall-through default: throw new Exception\InvalidArgumentException('expected DIRECTIVE'); } - state_value: switch (static::match(['="[^"]*"', '=[^",\s;]*'], $value, $lastMatch)) { case 0: $directives[$directive] = substr($lastMatch, 2, -1); diff --git a/src/Header/ContentSecurityPolicy.php b/src/Header/ContentSecurityPolicy.php index 21402634e2..3fd4304986 100644 --- a/src/Header/ContentSecurityPolicy.php +++ b/src/Header/ContentSecurityPolicy.php @@ -124,7 +124,7 @@ public function setDirective($name, array $sources) return $this; } - array_walk($sources, [__NAMESPACE__ . '\HeaderValue', 'assertValid']); + array_walk($sources, [HeaderValue::class, 'assertValid']); $this->directives[$name] = implode(' ', $sources); return $this; diff --git a/src/Header/ContentType.php b/src/Header/ContentType.php index 5de3572890..6c68326bb4 100644 --- a/src/Header/ContentType.php +++ b/src/Header/ContentType.php @@ -7,8 +7,6 @@ namespace Zend\Http\Header; -use stdClass; - /** * @throws Exception\InvalidArgumentException * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17 @@ -16,7 +14,7 @@ class ContentType implements HeaderInterface { /** - * @var string + * @var null|string */ protected $mediaType; @@ -26,7 +24,7 @@ class ContentType implements HeaderInterface protected $parameters = []; /** - * @var string + * @var null|string */ protected $value; @@ -49,6 +47,7 @@ public static function fromString($headerLine) } $parts = explode(';', $value); + /** @var string $mediaType */ $mediaType = array_shift($parts); $header = new static($value, trim($mediaType)); @@ -168,7 +167,7 @@ public function setMediaType($mediaType) /** * Get the media type * - * @return string + * @return null|string */ public function getMediaType() { @@ -238,7 +237,7 @@ protected function assembleValue() { $mediaType = $this->getMediaType(); if (empty($this->parameters)) { - return $mediaType; + return $mediaType ?: ''; } $parameters = []; @@ -275,8 +274,8 @@ function (&$value) { * - subtype * - format * - * @param string $string - * @return stdClass + * @param null|string $string + * @return object */ protected function getMediaTypeObjectFromString($string) { @@ -287,15 +286,18 @@ protected function getMediaTypeObjectFromString($string) )); } + /** @var string[] $parts */ $parts = explode('/', $string, 2); - if (1 == count($parts)) { + if (1 === count($parts)) { throw new Exception\DomainException(sprintf( 'Invalid mediatype "%s" provided', $string )); } + /** @var string $type */ $type = array_shift($parts); + /** @var string $subtype */ $subtype = array_shift($parts); $format = $subtype; if (false !== strpos($subtype, '+')) { @@ -316,8 +318,8 @@ protected function getMediaTypeObjectFromString($string) /** * Validate a subtype * - * @param stdClass $right - * @param stdClass $left + * @param object $right + * @param object $left * @return bool */ protected function validateSubtype($right, $left) @@ -356,17 +358,17 @@ protected function validateSubtype($right, $left) * * Validate that the right side format matches what the left side defines. * - * @param string $right - * @param string $left + * @param object $right + * @param object $left * @return bool */ protected function validateFormat($right, $left) { if ($right->format && $left->format) { - if ($right->format == '*') { + if ($right->format === '*') { return true; } - if ($right->format == $left->format) { + if ($right->format === $left->format) { return true; } return false; diff --git a/src/Header/Cookie.php b/src/Header/Cookie.php index 3cae6701a0..193397b384 100644 --- a/src/Header/Cookie.php +++ b/src/Header/Cookie.php @@ -29,6 +29,13 @@ public static function fromSetCookieArray(array $setCookies) )); } + if (null === $setCookie->getName()) { + throw new Exception\InvalidArgumentException(sprintf( + '%s requires cookies with name', + __METHOD__ + )); + } + if (array_key_exists($setCookie->getName(), $nvPairs)) { throw new Exception\InvalidArgumentException(sprintf( 'Two cookies with the same name were provided to %s', @@ -53,12 +60,13 @@ public static function fromString($headerLine) throw new Exception\InvalidArgumentException('Invalid header line for Server string: "' . $name . '"'); } + /** @var string[] $nvPairs */ $nvPairs = preg_split('#;\s*#', $value); $arrayInfo = []; foreach ($nvPairs as $nvPair) { $parts = explode('=', $nvPair, 2); - if (count($parts) != 2) { + if (count($parts) !== 2) { throw new Exception\RuntimeException('Malformed Cookie header found'); } list($name, $value) = $parts; @@ -104,7 +112,7 @@ public function getFieldValue() $nvPairs = []; foreach ($this->flattenCookies($this) as $name => $value) { - $nvPairs[] = $name . '=' . (($this->encodeValue) ? urlencode($value) : $value); + $nvPairs[] = $name . '=' . ($this->encodeValue ? urlencode($value) : $value); } return implode('; ', $nvPairs); diff --git a/src/Header/GenericMultiHeader.php b/src/Header/GenericMultiHeader.php index 18cdab61e4..5c773280b8 100644 --- a/src/Header/GenericMultiHeader.php +++ b/src/Header/GenericMultiHeader.php @@ -9,6 +9,10 @@ class GenericMultiHeader extends GenericHeader implements MultipleHeaderInterface { + /** + * @param string $headerLine + * @return GenericMultiHeader|GenericMultiHeader[] + */ public static function fromString($headerLine) { list($fieldName, $fieldValue) = GenericHeader::splitHeaderLine($headerLine); diff --git a/src/Header/HeaderValue.php b/src/Header/HeaderValue.php index a0cfe2ba9d..989d80237d 100644 --- a/src/Header/HeaderValue.php +++ b/src/Header/HeaderValue.php @@ -26,7 +26,7 @@ private function __construct() * between visible characters. * * @see http://en.wikipedia.org/wiki/HTTP_response_splitting - * @param string $value + * @param null|string $value * @return string */ public static function filter($value) @@ -63,7 +63,7 @@ public static function filter($value) * between visible characters. * * @see http://en.wikipedia.org/wiki/HTTP_response_splitting - * @param string $value + * @param null|string $value * @return bool */ public static function isValid($value) @@ -92,7 +92,7 @@ public static function isValid($value) /** * Assert a header value is valid. * - * @param string $value + * @param null|string $value * @throws Exception\RuntimeException for invalid values * @return void */ diff --git a/src/Header/RetryAfter.php b/src/Header/RetryAfter.php index 621c6fc448..1aefa93365 100644 --- a/src/Header/RetryAfter.php +++ b/src/Header/RetryAfter.php @@ -43,7 +43,7 @@ public static function fromString($headerLine) } if (is_numeric($date)) { - $dateHeader->setDeltaSeconds($date); + $dateHeader->setDeltaSeconds((int) $date); } else { $dateHeader->setDate($date); } diff --git a/src/Header/SetCookie.php b/src/Header/SetCookie.php index 5fbe32af50..ffcbbc5bed 100644 --- a/src/Header/SetCookie.php +++ b/src/Header/SetCookie.php @@ -125,7 +125,7 @@ class SetCookie implements MultipleHeaderInterface /** * @static * @throws Exception\InvalidArgumentException - * @param $headerLine + * @param string $headerLine * @param bool $bypassHeaderFieldName * @return array|SetCookie */ @@ -138,6 +138,7 @@ public static function fromString($headerLine, $bypassHeaderFieldName = false) $setCookieProcessor = function ($headerLine) use ($setCookieClass) { /** @var SetCookie $header */ $header = new $setCookieClass(); + /** @var string[] $keyValuePairs */ $keyValuePairs = preg_split('#;\s*#', $headerLine); foreach ($keyValuePairs as $keyValue) { @@ -207,17 +208,18 @@ public static function fromString($headerLine, $bypassHeaderFieldName = false) throw new Exception\InvalidArgumentException('Invalid header line for Set-Cookie string: "' . $name . '"'); } + /** @var string[] $multipleHeaders */ $multipleHeaders = preg_split('#(?type = 'Cookie'; - $this->setName($name) ->setValue($value) ->setVersion($version) @@ -296,7 +296,9 @@ public function getFieldValue() return ''; } - $value = $this->encodeValue ? urlencode($this->getValue()) : $this->getValue(); + $value = $this->getValue() ?: ''; + $value = $this->encodeValue ? urlencode($value) : $value; + if ($this->hasQuoteFieldValue()) { $value = '"' . $value . '"'; } @@ -405,7 +407,7 @@ public function getVersion() } /** - * @param int $maxAge + * @param int|null $maxAge * @return $this */ public function setMaxAge($maxAge) @@ -567,7 +569,7 @@ public function setHttponly($httponly) /** * @return bool|null */ - public function isHttponly() + public function isHttpOnly() { return $this->httponly; } @@ -646,11 +648,13 @@ public function hasQuoteFieldValue() */ public function isValidForRequest($requestDomain, $path, $isSecure = false) { - if ($this->getDomain() && (strrpos($requestDomain, $this->getDomain()) === false)) { + $cookieDomain = $this->getDomain(); + if ($cookieDomain && (strrpos($requestDomain, $cookieDomain) === false)) { return false; } - if ($this->getPath() && (strpos($path, $this->getPath()) !== 0)) { + $cookiePath = $this->getPath(); + if ($cookiePath && (strpos($path, $cookiePath) !== 0)) { return false; } @@ -677,12 +681,12 @@ public function match($uri, $matchSessionCookies = true, $now = null) } // Make sure we have a valid Zend_Uri_Http object - if (! ($uri->isValid() && ($uri->getScheme() == 'http' || $uri->getScheme() == 'https'))) { + if (! ($uri->isValid() && ($uri->getScheme() === 'http' || $uri->getScheme() === 'https'))) { throw new Exception\InvalidArgumentException('Passed URI is not a valid HTTP or HTTPS URI'); } // Check that the cookie is secure (if required) and not expired - if ($this->secure && $uri->getScheme() != 'https') { + if ($this->secure && $uri->getScheme() !== 'https') { return false; } if ($this->isExpired($now)) { @@ -693,11 +697,15 @@ public function match($uri, $matchSessionCookies = true, $now = null) } // Check if the domain matches - if (! self::matchCookieDomain($this->getDomain(), $uri->getHost())) { + if (! self::matchCookieDomain($this->getDomain() ?: '', $uri->getHost() ?: '')) { return false; } // Check that path matches using prefix match + if (null === $this->getPath() || null === $uri->getPath()) { + return false; + } + if (! self::matchCookiePath($this->getPath(), $uri->getPath())) { return false; } diff --git a/src/Headers.php b/src/Headers.php index 96153c2b83..0a285953ba 100644 --- a/src/Headers.php +++ b/src/Headers.php @@ -14,6 +14,7 @@ use Zend\Http\Header\Exception; use Zend\Http\Header\GenericHeader; use Zend\Loader\PluginClassLocator; +use Zend\Http\Header\MultipleHeaderInterface; /** * Basic HTTP headers collection functionality @@ -68,13 +69,13 @@ public static function fromString($string) continue; } - if ($emptyLine) { + if ($emptyLine > 0) { throw new Exception\RuntimeException('Malformed header detected'); } // check if a header name is present if (preg_match('/^(?P[^()><@,;:\"\\/\[\]?={} \t]+):.*$/', $line, $matches)) { - if ($current) { + if (isset($current['name'])) { // a header name was present, then store the current complete line $headers->headersKeys[] = static::createKey($current['name']); $headers->headers[] = $current; @@ -89,8 +90,10 @@ public static function fromString($string) if (preg_match("/^[ \t][^\r\n]*$/", $line, $matches)) { // continuation: append to current line - $current['line'] .= trim($line); - continue; + if (isset($current['line'])) { + $current['line'] .= trim($line); + continue; + } } // Line does not match header format! @@ -99,7 +102,8 @@ public static function fromString($string) $line )); } - if ($current) { + + if (isset($current['name'])) { $headers->headersKeys[] = static::createKey($current['name']); $headers->headers[] = $current; } @@ -154,7 +158,9 @@ public function addHeaders($headers) if (is_string($value)) { $this->addHeaderLine($value); } elseif (is_array($value) && count($value) == 1) { - $this->addHeaderLine(key($value), current($value)); + /** @var string */ + $key = key($value); + $this->addHeaderLine($key, current($value)); } elseif (is_array($value) && count($value) == 2) { $this->addHeaderLine($value[0], $value[1]); } elseif ($value instanceof Header\HeaderInterface) { @@ -282,9 +288,9 @@ public function get($name) return false; } - $class = ($this->getPluginClassLoader()->load(str_replace('-', '', $key))) ?: 'Zend\Http\Header\GenericHeader'; + $class = ($this->getPluginClassLoader()->load(str_replace('-', '', $key))) ?: GenericHeader::class; - if (in_array('Zend\Http\Header\MultipleHeaderInterface', class_implements($class, true))) { + if (in_array(MultipleHeaderInterface::class, class_implements($class, true))) { $headers = []; foreach (array_keys($this->headersKeys, $key) as $index) { if (is_array($this->headers[$index])) { @@ -368,8 +374,9 @@ public function rewind() public function current() { $current = current($this->headers); - if (is_array($current)) { - $current = $this->lazyLoadHeader(key($this->headers)); + $key = key($this->headers); + if (null !== $key && is_array($current)) { + $current = $this->lazyLoadHeader($key); } return $current; } @@ -452,9 +459,9 @@ public function forceLoading() } /** - * @param $index + * @param string|int $index * @param bool $isGeneric If true, there is no need to parse $index and call the ClassLoader. - * @return mixed|void + * @return Header\HeaderInterface */ protected function lazyLoadHeader($index, $isGeneric = false) { diff --git a/src/PhpEnvironment/RemoteAddress.php b/src/PhpEnvironment/RemoteAddress.php index 65a001993a..1d8dde2327 100644 --- a/src/PhpEnvironment/RemoteAddress.php +++ b/src/PhpEnvironment/RemoteAddress.php @@ -94,6 +94,7 @@ public function setProxyHeader($header = 'X-Forwarded-For') */ public function getIpAddress() { + /** @var string|false $ip */ $ip = $this->getIpAddressFromProxy(); if ($ip) { return $ip; @@ -111,7 +112,7 @@ public function getIpAddress() * Attempt to get the IP address for a proxied client * * @see http://tools.ietf.org/html/draft-ietf-appsawg-http-forwarded-10#section-5.2 - * @return false|string + * @return bool|string */ protected function getIpAddressFromProxy() { @@ -131,6 +132,7 @@ protected function getIpAddressFromProxy() // trim, so we can compare against trusted proxies properly $ips = array_map('trim', $ips); // remove trusted proxy IPs + /** @var string[] $ips */ $ips = array_diff($ips, $this->trustedProxies); // Any left? diff --git a/src/PhpEnvironment/Request.php b/src/PhpEnvironment/Request.php index 4101af1752..b929a5d1dc 100644 --- a/src/PhpEnvironment/Request.php +++ b/src/PhpEnvironment/Request.php @@ -8,6 +8,8 @@ namespace Zend\Http\PhpEnvironment; use Zend\Http\Header\Cookie; +use Zend\Http\Header\HeaderInterface; +use Zend\Http\Headers; use Zend\Http\Request as HttpRequest; use Zend\Stdlib\Parameters; use Zend\Stdlib\ParametersInterface; @@ -93,7 +95,7 @@ public function getContent() { if (empty($this->content)) { $requestBody = file_get_contents('php://input'); - if (strlen($requestBody) > 0) { + if ($requestBody && strlen($requestBody) > 0) { $this->content = $requestBody; } } @@ -106,12 +108,14 @@ public function getContent() * * Instantiate and set cookies. * - * @param $cookie + * @param ParametersInterface $cookie * @return $this */ public function setCookies($cookie) { - $this->getHeaders()->addHeader(new Cookie((array) $cookie)); + /** @var Headers $headers */ + $headers = $this->getHeaders(); + $headers->addHeader(new Cookie((array) $cookie)); return $this; } @@ -205,7 +209,7 @@ public function setServer(ParametersInterface $server) // This seems to be the only way to get the Authorization header on Apache if (function_exists('apache_request_headers')) { $apacheRequestHeaders = apache_request_headers(); - if (! isset($this->serverParams['HTTP_AUTHORIZATION'])) { + if (false !== $apacheRequestHeaders && ! isset($this->serverParams['HTTP_AUTHORIZATION'])) { if (isset($apacheRequestHeaders['Authorization'])) { $this->serverParams->set('HTTP_AUTHORIZATION', $apacheRequestHeaders['Authorization']); } elseif (isset($apacheRequestHeaders['authorization'])) { @@ -215,7 +219,7 @@ public function setServer(ParametersInterface $server) } // set headers - $headers = []; + $headersArray = []; foreach ($server as $key => $value) { if ($value || (! is_array($value) && strlen($value))) { @@ -225,15 +229,17 @@ public function setServer(ParametersInterface $server) continue; } - $headers[strtr(ucwords(strtolower(strtr(substr($key, 5), '_', ' '))), ' ', '-')] = $value; + $headersArray[strtr(ucwords(strtolower(strtr(substr($key, 5), '_', ' '))), ' ', '-')] = $value; } elseif (strpos($key, 'CONTENT_') === 0) { $name = substr($key, 8); // Remove "Content-" - $headers['Content-' . (($name == 'MD5') ? $name : ucfirst(strtolower($name)))] = $value; + $headersArray['Content-' . (($name === 'MD5') ? $name : ucfirst(strtolower($name)))] = $value; } } } - $this->getHeaders()->addHeaders($headers); + /** @var Headers $headers */ + $headers = $this->getHeaders(); + $headers->addHeaders($headersArray); // set method if (isset($this->serverParams['REQUEST_METHOD'])) { @@ -266,9 +272,10 @@ public function setServer(ParametersInterface $server) $port = null; // Set the host - $headerHost = $this->getHeaders()->get('host'); - if ($headerHost) { - $host = $headerHost->getFieldValue(); + /** @var false|HeaderInterface $hostHeader */ + $hostHeader = $headers->get('host'); + if ($hostHeader) { + $host = $hostHeader->getFieldValue(); // works for regname, IPv4 & IPv6 if (preg_match('|\:(\d+)$|', $host, $matches)) { @@ -310,7 +317,8 @@ public function setServer(ParametersInterface $server) // URI path $requestUri = $this->getRequestUri(); - if (($qpos = strpos($requestUri, '?')) !== false) { + $qpos = strpos($requestUri, '?'); + if ($qpos !== false) { $requestUri = substr($requestUri, 0, $qpos); } @@ -448,7 +456,9 @@ protected function detectRequestUri() // HTTP proxy requests setup request URI with scheme and host [and port] // + the URL path, only use URL path. if ($requestUri !== null) { - return preg_replace('#^[^/:]+://[^/]+#', '', $requestUri); + /** @var string $uri */ + $uri = preg_replace('#^[^/:]+://[^/]+#', '', $requestUri); + return $uri; } // IIS 5.0, PHP as CGI. @@ -529,7 +539,8 @@ protected function detectBaseUrl() $truncatedRequestUri = $requestUri; - if (false !== ($pos = strpos($requestUri, '?'))) { + $pos = strpos($requestUri, '?'); + if (false !== $pos) { $truncatedRequestUri = substr($requestUri, 0, $pos); } diff --git a/src/PhpEnvironment/Response.php b/src/PhpEnvironment/Response.php index 0a926b20c1..294b12352b 100644 --- a/src/PhpEnvironment/Response.php +++ b/src/PhpEnvironment/Response.php @@ -96,7 +96,6 @@ public function sendHeaders() header($header->toString()); } - $this->headersSent = true; return $this; } diff --git a/src/Request.php b/src/Request.php index 8b9cd6f277..bb7d70364a 100644 --- a/src/Request.php +++ b/src/Request.php @@ -7,6 +7,7 @@ namespace Zend\Http; +use Zend\Http\Header\HeaderInterface; use Zend\Stdlib\Parameters; use Zend\Stdlib\ParametersInterface; use Zend\Stdlib\RequestInterface; @@ -100,6 +101,7 @@ public static function fromString($string, $allowCustomMethods = true) ); $regex = '#^(?P' . $methods . ')\s(?P[^ ]*)(?:\sHTTP\/(?P\d+\.\d+)){0,1}#'; + /** @var string $firstLine */ $firstLine = array_shift($lines); if (! preg_match($regex, $firstLine, $matches)) { throw new Exception\InvalidArgumentException( @@ -111,6 +113,13 @@ public static function fromString($string, $allowCustomMethods = true) $request->setUri($matches['uri']); $parsedUri = parse_url($matches['uri']); + + if (false === $parsedUri) { + throw new Exception\InvalidArgumentException( + 'A valid request line was not found in the provided string' + ); + } + if (array_key_exists('query', $parsedUri)) { $parsedQuery = []; parse_str($parsedUri['query'], $parsedQuery); @@ -128,6 +137,7 @@ public static function fromString($string, $allowCustomMethods = true) $isHeader = true; $headers = $rawBody = []; while ($lines) { + /** @var string $nextLine */ $nextLine = array_shift($lines); if ($nextLine == '') { $isHeader = false; @@ -213,6 +223,13 @@ public function setUri($uri) 'URI must be an instance of Zend\Uri\Http or a string' ); } + + $path = $uri->getPath(); + + if (empty($path)) { + $uri->setPath('/'); + } + $this->uri = $uri; return $this; @@ -318,7 +335,12 @@ public function getPost($name = null, $default = null) */ public function getCookie() { - return $this->getHeaders()->get('Cookie'); + /** @var Headers $headers */ + $headers = $this->getHeaders(); + /** @var false|Header\Cookie $header */ + $header = $headers->get('Cookie'); + + return $header; } /** @@ -366,7 +388,7 @@ public function getHeaders($name = null, $default = false) { if ($this->headers === null || is_string($this->headers)) { // this is only here for fromString lazy loading - $this->headers = (is_string($this->headers)) ? Headers::fromString($this->headers) : new Headers(); + $this->headers = is_string($this->headers) ? Headers::fromString($this->headers) : new Headers(); } if ($name === null) { @@ -390,7 +412,10 @@ public function getHeaders($name = null, $default = false) */ public function getHeader($name, $default = false) { - return $this->getHeaders($name, $default); + /** @var false|HeaderInterface $header */ + $header = $this->getHeaders($name, $default); + + return $header; } /** @@ -502,8 +527,11 @@ public function isPatch() */ public function isXmlHttpRequest() { - $header = $this->getHeaders()->get('X_REQUESTED_WITH'); - return false !== $header && $header->getFieldValue() == 'XMLHttpRequest'; + /** @var Headers $headers */ + $headers = $this->getHeaders(); + /** @var false|HeaderInterface $header */ + $header = $headers->get('X_REQUESTED_WITH'); + return false !== $header && $header->getFieldValue() === 'XMLHttpRequest'; } /** @@ -513,7 +541,10 @@ public function isXmlHttpRequest() */ public function isFlashRequest() { - $header = $this->getHeaders()->get('USER_AGENT'); + /** @var Headers $headers */ + $headers = $this->getHeaders(); + /** @var false|HeaderInterface $header */ + $header = $headers->get('USER_AGENT'); return false !== $header && stristr($header->getFieldValue(), ' flash'); } @@ -532,8 +563,10 @@ public function renderRequestLine() */ public function toString() { + /** @var Headers $headers */ + $headers = $this->getHeaders(); $str = $this->renderRequestLine() . "\r\n"; - $str .= $this->getHeaders()->toString(); + $str .= $headers->toString(); $str .= "\r\n"; $str .= $this->getContent(); return $str; diff --git a/src/Response.php b/src/Response.php index a64dbf3d07..64a25a8466 100644 --- a/src/Response.php +++ b/src/Response.php @@ -7,7 +7,8 @@ namespace Zend\Http; -use Zend\Http\Exception\RuntimeException; +use Zend\Http\Exception\InvalidArgumentException; +use Zend\Http\Header\HeaderInterface; use Zend\Stdlib\ErrorHandler; use Zend\Stdlib\ResponseInterface; @@ -197,6 +198,7 @@ public static function fromString($string) $lines = explode("\n", $string); } + /** @var string $firstLine */ $firstLine = array_shift($lines); $response = new static(); @@ -208,6 +210,9 @@ public static function fromString($string) if ($response->statusCode === static::STATUS_CODE_100) { $next = array_shift($lines); // take next line $next = empty($next) ? array_shift($lines) : $next; // take next or skip if empty + if (null === $next) { + throw new Exception\InvalidArgumentException('Invalid response content'); + } $response->parseStatusLine($next); } @@ -273,11 +278,14 @@ protected function parseStatusLine($line) } /** - * @return Header\SetCookie[] + * @return bool|Header\SetCookie[] */ public function getCookie() { - return $this->getHeaders()->get('Set-Cookie'); + /** @var false|Header\SetCookie[] $header */ + $header = $this->getHeaders()->get('Set-Cookie'); + + return $header; } /** @@ -361,7 +369,7 @@ public function setReasonPhrase($reasonPhrase) /** * Get HTTP status message * - * @return string + * @return null|string */ public function getReasonPhrase() { @@ -380,14 +388,17 @@ public function getBody() { $body = (string) $this->getContent(); + /** @var false|HeaderInterface $transferEncoding */ $transferEncoding = $this->getHeaders()->get('Transfer-Encoding'); if (! empty($transferEncoding)) { - if (strtolower($transferEncoding->getFieldValue()) === 'chunked') { + $transferEncoding = $transferEncoding->getFieldValue(); + if (strtolower($transferEncoding) === 'chunked') { $body = $this->decodeChunkedBody($body); } } + /** @var false|HeaderInterface $contentEncoding */ $contentEncoding = $this->getHeaders()->get('Content-Encoding'); if (! empty($contentEncoding)) { @@ -552,7 +563,7 @@ protected function decodeChunkedBody($body) break; } - $length = hexdec(trim($m[1])); + $length = (int) hexdec(trim($m[1])); $cut = strlen($m[0]); $decBody .= substr($body, $offset + $cut, $length); $offset += $cut + $length + 2; @@ -578,9 +589,10 @@ protected function decodeGzip($body) ); } + $contentLengthHeader = $this->getHeaders()->get('content-length'); if ($body === '' - || ($this->getHeaders()->has('content-length') - && (int) $this->getHeaders()->get('content-length')->getFieldValue() === 0) + || ($contentLengthHeader instanceof Header\ContentLength + && (int) $contentLengthHeader->getFieldValue() === 0) ) { return ''; } @@ -588,7 +600,7 @@ protected function decodeGzip($body) ErrorHandler::start(); $return = gzinflate(substr($body, 10)); $test = ErrorHandler::stop(); - if ($test) { + if ($test || false === $return) { throw new Exception\RuntimeException( 'Error occurred during gzip inflation', 0, @@ -615,8 +627,9 @@ protected function decodeDeflate($body) ); } - if ($this->getHeaders()->has('content-length') - && 0 === (int) $this->getHeaders()->get('content-length')->getFieldValue()) { + $contentLengthHeader = $this->getHeaders()->get('content-length'); + if ($contentLengthHeader instanceof Header\ContentLength + && 0 === (int) $contentLengthHeader->getFieldValue()) { return ''; } @@ -634,8 +647,17 @@ protected function decodeDeflate($body) $zlibHeader = unpack('n', substr($body, 0, 2)); if ($zlibHeader[1] % 31 === 0) { - return gzuncompress($body); + $uncompressed = gzuncompress($body); + } else { + $uncompressed = gzinflate($body); + } + + if (false === $uncompressed) { + throw new Exception\RuntimeException( + 'An error occurred during inflation' + ); } - return gzinflate($body); + + return $uncompressed; } } diff --git a/src/Response/Stream.php b/src/Response/Stream.php index 371c0cc35e..3833b87ed6 100644 --- a/src/Response/Stream.php +++ b/src/Response/Stream.php @@ -19,7 +19,7 @@ class Stream extends Response /** * The Content-Length value, if set * - * @var int + * @var null|int */ protected $contentLength; @@ -33,7 +33,7 @@ class Stream extends Response /** * Response as stream * - * @var resource + * @var null|resource */ protected $stream; @@ -42,7 +42,7 @@ class Stream extends Response * * Will be empty if stream is not file-based. * - * @var string + * @var null|string */ protected $streamName; @@ -51,7 +51,7 @@ class Stream extends Response * * @var bool */ - protected $cleanup; + protected $cleanup = false; /** * Set content length @@ -76,7 +76,7 @@ public function getContentLength() /** * Get the response as stream * - * @return resource + * @return null|resource */ public function getStream() { @@ -118,7 +118,7 @@ public function setCleanup($cleanup = true) /** * Get file name associated with the stream * - * @return string + * @return null|string */ public function getStreamName() { @@ -161,22 +161,25 @@ public static function fromStream($responseString, $stream) } while (! empty($responseArray)) { + /** @var string $nextLine */ $nextLine = array_shift($responseArray); $headersString .= $nextLine . "\n"; $nextLineTrimmed = trim($nextLine); - if ($nextLineTrimmed == '') { + if ($nextLineTrimmed === '') { $headerComplete = true; break; } } if (! $headerComplete) { - while (false !== ($nextLine = fgets($stream))) { + $nextLine = fgets($stream); + while (false !== $nextLine) { $headersString .= trim($nextLine) . "\r\n"; - if ($nextLine == "\r\n" || $nextLine == "\n") { + if ($nextLine === "\r\n" || $nextLine === "\n") { $headerComplete = true; break; } + $nextLine = fgets($stream); } } @@ -266,14 +269,14 @@ protected function readStream() $bytes = -1; // Read the whole buffer } - if (! is_resource($this->stream) || $bytes == 0) { + if (! is_resource($this->stream) || $bytes === 0) { return ''; } $this->content .= stream_get_contents($this->stream, $bytes); $this->contentStreamed += strlen($this->content); - if ($this->getContentLength() == $this->contentStreamed) { + if ($this->getContentLength() === $this->contentStreamed) { $this->stream = null; } } @@ -286,7 +289,7 @@ public function __destruct() if (is_resource($this->stream)) { $this->stream = null; //Could be listened by others } - if ($this->cleanup) { + if ($this->cleanup && $this->streamName) { ErrorHandler::start(E_WARNING); unlink($this->streamName); ErrorHandler::stop(); diff --git a/test/ClientTest.php b/test/ClientTest.php index 1efbb45b83..6da7259de9 100644 --- a/test/ClientTest.php +++ b/test/ClientTest.php @@ -9,6 +9,7 @@ use ArrayIterator; use PHPUnit\Framework\TestCase; +use ReflectionClass; use ReflectionMethod; use Zend\Http\Client; use Zend\Http\Client\Adapter\AdapterInterface; @@ -24,10 +25,30 @@ use Zend\Http\Request; use Zend\Http\Response; use Zend\Uri\Http; +use Zend\Stdlib; use ZendTest\Http\TestAsset\ExtendedClient; class ClientTest extends TestCase { + private $originalErrorReporting; + private $tmpDir; + + protected function setUp() + { + $this->originalErrorReporting = \error_reporting(); + $this->tmpDir = \getenv('TMPDIR'); + + parent::setUp(); + } + + protected function tearDown() + { + \error_reporting($this->originalErrorReporting); + \putenv('TMPDIR=' . $this->tmpDir); + + parent::tearDown(); + } + public function testIfCookiesAreSticky() { $initialCookies = [ @@ -119,6 +140,8 @@ public function testIfZeroValueCookiesCanBeSet() $client->addCookie('test', 0); $client->addCookie('test2', '0'); $client->addCookie('test3', false); + + $this->assertCount(3, $client->getCookies()); } public function testIfNullValueCookiesThrowsException() @@ -179,6 +202,17 @@ public function testArgSeparatorDefaultsToIniSetting() $this->assertEquals($argSeparator, $client->getArgSeparator()); } + /** + * @group 2774 + * @group 2745 + */ + public function testArgSeparatorDefaultsWithNoIniSetting() + { + \ini_set('arg_separator.output', false); + $client = new Client(); + $this->assertEquals('&', $client->getArgSeparator()); + } + /** * @group 2774 * @group 2745 @@ -192,7 +226,7 @@ public function testCanOverrideArgSeparator() public function testClientUsesAcceptEncodingHeaderFromRequestObject() { - $client = new Client(); + $client = new Client('http://foo.com'); $client->setAdapter(Test::class); @@ -266,6 +300,75 @@ public function testIfMaxredirectWorksCorrectly() $this->assertEquals($response->getContent(), 'Page #2'); } + public function testSendWithNotUriPath() + { + $testAdapter = new Test(); + $testAdapter->setResponse( + 'HTTP/1.1 200 OK' . "\r\n\r\n" + . 'Page #1' + ); + + $uri = new Http(); + $uri->setHost('www.example.org'); + + $this->assertNull($uri->getPath()); + + $client = new Client($uri, [ + 'adapter' => $testAdapter, + 'storeresponse' => true, + ]); + + // do the request + $response = $client->setMethod('GET')->send(); + + $this->assertEquals($response->getContent(), 'Page #1'); + } + + public function testHappyPathWithDispatch() + { + $testAdapter = new Test(); + $testAdapter->setResponse( + 'HTTP/1.1 200 OK' . "\r\n\r\n" + . 'Page #1' + ); + + $client = new Client('http://www.example.org/part1', [ + 'adapter' => $testAdapter, + 'storeresponse' => true, + ]); + + $request = new Request(); + $request->setUri('http://www.example.org/part1'); + $response = new Response(); + + // do the request + $response = $client->setMethod('GET')->dispatch($request, $response); + + $this->assertEquals($response->getContent(), 'Page #1'); + } + + public function testDispatchWithBaseInterface() + { + $this->expectException(HttpException\UnexpectedValueException::class); + + $testAdapter = new Test(); + $testAdapter->setResponse( + 'HTTP/1.1 200 OK' . "\r\n\r\n" + . 'Page #1' + ); + + $client = new Client('http://www.example.org/part1', [ + 'adapter' => $testAdapter, + 'storeresponse' => true, + ]); + + $request = $this->prophesize(Stdlib\RequestInterface::class); + $response = new Response(); + + // do the request + $client->setMethod('GET')->dispatch($request->reveal(), $response); + } + public function testIfClientDoesNotLooseAuthenticationOnRedirect() { // set up user credentials @@ -486,6 +589,7 @@ public function testHttpQueryParametersCastToString() public function testClientRequestMethod() { $request = new Request(); + $request->setUri('http://foo.com'); $request->setMethod(Request::METHOD_POST); $request->getPost()->set('data', 'random'); @@ -507,7 +611,7 @@ public function testAllowsClearingEncType() $this->assertEquals('application/x-www-form-urlencoded', $client->getEncType()); $client->setEncType(null); - $this->assertNull($client->getEncType()); + $this->assertSame('', $client->getEncType()); } /** @@ -518,6 +622,7 @@ public function testFormUrlEncodeSeparator() $client = new Client(); $client->setEncType('application/x-www-form-urlencoded'); $request = new Request(); + $request->setUri('http://foo.com'); $request->setMethod(Request::METHOD_POST); $request->getPost()->set('foo', 'bar'); $request->getPost()->set('baz', 'foo'); @@ -528,25 +633,30 @@ public function testFormUrlEncodeSeparator() $this->assertContains('foo=bar&baz=foo', $rawRequest); } - public function uriDataProvider() + public function testRelativeUriInConstructorIsNotAllowed() { - return [ - 'valid-relative' => ['/example', true], - 'invalid-absolute' => ['http://localhost/example', false], - ]; + $this->expectException(HttpException\InvalidArgumentException::class); + $client = new Client('/example'); } - /** - * @dataProvider uriDataProvider - */ - public function testUriCorrectlyDeterminesWhetherOrNotItIsAValidRelativeUri($uri, $isValidRelativeURI) + public function testRelativeUriIsNotAllowed() + { + $this->expectException(HttpException\InvalidArgumentException::class); + $client = new Client('http://www.domain.com'); + $client->setUri('/example'); + } + + public function testRelativeUriIsNotAllowedSendingRequest() { - $client = new Client($uri); - $this->assertSame($isValidRelativeURI, $client->getUri()->isValidRelative()); + $this->expectException(HttpException\InvalidArgumentException::class); + $client = new Client(); + $uri = new Http(); + $request = new Request(); + $request->setUri($uri); + $request->setMethod(Request::METHOD_GET); $client->setAdapter(Test::class); - $client->send(); - $this->assertSame($isValidRelativeURI, $client->getUri()->isValidRelative()); + $client->send($request); } public function portChangeDataProvider() @@ -637,4 +747,151 @@ public function testStreamCompression(AdapterInterface $adapter) self::assertSame($response->getBody(), file_get_contents($tmpFile)); } + + public function testClientRequestWillNotThrowExceptionOnResponseWithNoCookies() + { + $request = new Request(); + $request->setUri('http://www.domain.com'); + $request->setMethod(Request::METHOD_POST); + $request->getPost()->set('data', 'random'); + + $response = new Response(); + + $adapter = new Test(); + + $adapter->setResponse([ + 0 => $response, + ]); + + $client = new Client(); + $client->setAdapter($adapter); + $client->send($request); + + $this->assertCount(0, $client->getCookies()); + } + + public function testClientRequestWillSaveCookiesAndReuseThem() + { + $cookies = new Cookies(); + $cookies->addCookie(new SetCookie('foo', 'far', null, '/', 'www.domain.com')); + $cookies->addCookie(new SetCookie('bar', 'far', null, '/', 'www.domain.com')); + + $request = new Request(); + $request->setUri('http://www.domain.com'); + $request->setMethod(Request::METHOD_POST); + $request->getPost()->set('data', 'random'); + + $response = new Response(); + $response->getHeaders()->addHeaders($cookies->getAllCookies()); + + $adapter = new Test(); + + $adapter->setResponse([ + 0 => $response, + ]); + + $client = new Client(); + $client->setAdapter($adapter); + $client->send($request); + + $this->assertCount(2, $client->getCookies()); + + $request2 = new Request(); + $request2->setUri('http://www.domain.com'); + + $client->send($request2); + + $lastRawRequest = $client->getLastRawRequest(); + $this->assertContains("\r\nCookie: foo=far; bar=far\r\n", $lastRawRequest); + } + + public function testClientRequestWillSaveCookiesAndReuseThemWithNullPathUri() + { + $cookies = new Cookies(); + $cookies->addCookie(new SetCookie('foo', 'far', null, '/', 'www.domain.com')); + //$cookies->addCookie(new SetCookie('foo2', 'far', null, '/foo', 'www.domain.com')); + $cookies->addCookie(new SetCookie('bar', 'far', null, '/', 'www.domain.com')); + + $uri = new Http(); + $uri->setHost('www.domain.com'); + + $this->assertNull($uri->getPath()); + + $request = new Request(); + $request->setUri('http://www.domain.com'); + $request->setMethod(Request::METHOD_POST); + $request->getPost()->set('data', 'random'); + + $response = new Response(); + $response->getHeaders()->addHeaders($cookies->getAllCookies()); + + $adapter = new Test(); + + $adapter->setResponse([ + 0 => $response, + ]); + + $client = new Client(); + $client->setAdapter($adapter); + $client->send($request); + + $this->assertCount(2, $client->getCookies()); + + $request2 = new Request(); + $request2->setUri($uri); + + $client->send($request2); + + $lastRawRequest = $client->getLastRawRequest(); + $this->assertContains("\r\nCookie: foo=far; bar=far\r\n", $lastRawRequest); + } + + public function testClientRequestWillNotReuseCookiesFromDifferentDomain() + { + $cookies = new Cookies(); + $cookies->addCookie(new SetCookie('foo', 'far', null, '/', 'www.domain.com')); + $cookies->addCookie(new SetCookie('bar', 'far', null, '/', 'www.domain.com')); + + $request = new Request(); + $request->setUri('http://www.domain.com'); + $request->setMethod(Request::METHOD_POST); + $request->getPost()->set('data', 'random'); + + $response = new Response(); + $response->getHeaders()->addHeaders($cookies->getAllCookies()); + + $adapter = new Test(); + + $adapter->setResponse([ + 0 => $response, + ]); + + $client = new Client(); + $client->setAdapter($adapter); + $client->send($request); + + $this->assertCount(2, $client->getCookies()); + + $request2 = new Request(); + $request2->setUri('http://foo.com'); + + $client->send($request2); + + $lastRawRequest = $client->getLastRawRequest(); + $this->assertNotContains("\r\nCookie: foo=far; bar=far\r\n", $lastRawRequest); + } + + public function testDoRequestWithNoHostUri() + { + $this->expectException(HttpException\InvalidArgumentException::class); + + $client = new Client(); + $class = new ReflectionClass(Client::class); + $method = $class->getMethod('doRequest'); + $method->setAccessible(true); + + $uri = new Http(); + + $method->invokeArgs($client, [$uri, 'GET']); + } } diff --git a/test/CookiesTest.php b/test/CookiesTest.php index 75e10f8297..d39e28bda8 100644 --- a/test/CookiesTest.php +++ b/test/CookiesTest.php @@ -44,6 +44,49 @@ public function testFromResponseInCookie() $this->assertSame($header, $response->getCookie('http://www.zend.com', 'foo')); } + public function testGetAllCookiesStringObject() + { + $response = new Response(); + $headers = new Headers(); + $header = new SetCookie('foo', 'bar'); + $header->setDomain('www.zend.com'); + $header->setPath('/'); + $header2 = new SetCookie('foo2', 'bar2'); + $header2->setDomain('www.zend2.com'); + $header2->setPath('/'); + $headers->addHeader($header); + $headers->addHeader($header2); + $response->setHeaders($headers); + + $response = Cookies::fromResponse($response, 'http://www.zend.com'); + $result = $response->getAllCookies(Cookies::COOKIE_OBJECT); + $this->assertSame([$header, $header2], $result); + } + + public function testGetAllCookiesStringArray() + { + $response = new Response(); + $headers = new Headers(); + $header = new SetCookie('foo', 'bar'); + $header->setDomain('www.zend.com'); + $header->setPath('/'); + $header2 = new SetCookie('foo2', 'bar2'); + $header2->setDomain('www.zend2.com'); + $header2->setPath('/'); + $headers->addHeader($header); + $headers->addHeader($header2); + $response->setHeaders($headers); + + $expected = [ + 'Set-Cookie: foo=bar; Domain=www.zend.com; Path=/', + 'Set-Cookie: foo2=bar2; Domain=www.zend2.com; Path=/', + ]; + + $response = Cookies::fromResponse($response, 'http://www.zend.com'); + $result = $response->getAllCookies(Cookies::COOKIE_STRING_ARRAY); + $this->assertSame($expected, $result); + } + public function testRequestCanHaveArrayCookies() { $_COOKIE = [ diff --git a/test/Exception/UnexpectedValueExceptionTest.php b/test/Exception/UnexpectedValueExceptionTest.php new file mode 100644 index 0000000000..f24daed560 --- /dev/null +++ b/test/Exception/UnexpectedValueExceptionTest.php @@ -0,0 +1,25 @@ +assertSame('Expected foo. ArrayObject given', $exception->getMessage()); + } + + public function testUnexpectedTypeWithScalarType() + { + $exception = UnexpectedValueException::unexpectedType('foo', 5); + + $this->assertSame('Expected foo. integer given', $exception->getMessage()); + } +} diff --git a/test/Header/SetCookieTest.php b/test/Header/SetCookieTest.php index aa35678e9e..ff89f9e761 100644 --- a/test/Header/SetCookieTest.php +++ b/test/Header/SetCookieTest.php @@ -146,7 +146,7 @@ public function testSetCookieFromStringCanCreateSingleHeader() $this->assertEquals('/accounts', $setCookieHeader->getPath()); $this->assertEquals('Wed, 13-Jan-2021 22:23:01 GMT', $setCookieHeader->getExpires()); $this->assertTrue($setCookieHeader->isSecure()); - $this->assertTrue($setCookieHeader->isHttponly()); + $this->assertTrue($setCookieHeader->isHttpOnly()); $setCookieHeader = SetCookie::fromString( 'set-cookie: myname=myvalue; Domain=docs.foo.com; Path=/accounts;' @@ -310,7 +310,7 @@ public function testSetCookieAttributesAreUnsettable() $this->assertNull($setCookieHeader->getDomain()); $this->assertNull($setCookieHeader->getPath()); $this->assertNull($setCookieHeader->isSecure()); - $this->assertNull($setCookieHeader->isHttponly()); + $this->assertNull($setCookieHeader->isHttpOnly()); } public function testSetCookieFieldValueIsEmptyStringWhenNameIsUnset() diff --git a/test/HeadersTest.php b/test/HeadersTest.php index d2ac2ba6ce..72e040dcda 100644 --- a/test/HeadersTest.php +++ b/test/HeadersTest.php @@ -441,4 +441,10 @@ public function testThrowExceptionOnInvalidHeader() $this->expectExceptionMessage('Invalid header value detected'); $headers->get('Location'); } + + public function testHeadersFromStringFactoryWithNoCurrentLineShouldThrowException() + { + $this->expectException(RuntimeException::class); + Headers::fromString(" Fake foo\r\n -bar"); + } } diff --git a/test/ResponseTest.php b/test/ResponseTest.php index b62aaa15e4..26008eff65 100644 --- a/test/ResponseTest.php +++ b/test/ResponseTest.php @@ -16,6 +16,23 @@ class ResponseTest extends TestCase { + + private $originalErrorReporting; + + protected function setUp() + { + $this->originalErrorReporting = \error_reporting(); + + parent::setUp(); + } + + protected function tearDown() + { + \error_reporting($this->originalErrorReporting); + + parent::tearDown(); + } + public function validHttpVersions() { yield 'http/1.0' => ['1.0']; @@ -226,6 +243,35 @@ public function testDeflateResponse() $this->assertEquals('ad62c21c3aa77b6a6f39600f6dd553b8', md5($res->getContent())); } + public function testDeflateResponseWithDeflateError() + { + \error_reporting(E_ALL ^ \E_STRICT ^ \E_WARNING); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('An error occurred during inflation'); + + $responseTest = <<<'REQ' +HTTP/1.1 200 OK +Date: Sun, 25 Jun 2006 19:38:02 GMT +Server: Apache +X-powered-by: PHP/5.1.4-pl3-gentoo +Content-encoding: deflate +Vary: Accept-Encoding +Content-length: 300 +Connection: close +Content-type: text/html + +REQ; + + // uncompressed data is more than 32768 times the length of the compressed input data + $data = \str_repeat('1', 10000000); + + $responseTest .= "\n" . \gzdeflate($data); + + $res = Response::fromString($responseTest); + $res->getBody(); + } + public function testDeflateResponseWithEmptyBody() { $responseTest = <<<'REQ' @@ -632,6 +678,14 @@ public function test100ContinueFromString() $this->assertEquals($fixture, $request->getBody()); } + public function test100ContinueWithInvalidContent() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid response content'); + + Response::fromString("HTTP/1.1 100 Continue\r\n"); + } + /** * Helper function: read test response from file *