From 6e2f340b2081fe2d4400a3b22bb9a44833b80319 Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Tue, 16 Oct 2018 11:36:56 +0200 Subject: [PATCH 01/21] phpstan fixes phpstan fixes phpstan fixes Removed deprecations Fixed wrong condition Better phpdoc types Better tests with relative datetimes Some fixes and removed false return types Replaced return values $this with self Replaced usage of stdClass with object in param doc Removed useless check and getter of adapter Fixed typo Reverted rename of method setHttponly, fixed usage with incorrect case PHPStan fixes Fix variable overwrite Reverted composer changes Fix and test header factory Added test for invalid 100 response Test gzinflate error Added test for default query argument separator Fixed missing char in phpdoc Added test to handle tmpname failure Added test for dispatch() method Fixed cookies usage --- .travis.yml | 4 + CHANGELOG.md | 5 +- phpstan.neon.dist | 18 ++ src/AbstractMessage.php | 2 +- src/Client.php | 212 +++++++++----- src/Client/Adapter/Curl.php | 70 ++--- .../Exception/UnexpectedValueException.php | 15 + src/Client/Adapter/Proxy.php | 26 +- src/Client/Adapter/Socket.php | 85 ++++-- src/Client/Adapter/StreamInterface.php | 2 +- .../Exception/UnexpectedValueException.php | 15 + src/ClientStatic.php | 12 +- src/Cookies.php | 41 +-- src/Exception/UnexpectedValueException.php | 26 ++ src/Header/AbstractAccept.php | 29 +- src/Header/AbstractDate.php | 22 +- src/Header/Accept.php | 4 +- .../FieldValuePart/AbstractFieldValuePart.php | 17 +- .../FieldValuePart/AcceptFieldValuePart.php | 6 +- .../FieldValuePart/CharsetFieldValuePart.php | 2 +- .../FieldValuePart/EncodingFieldValuePart.php | 2 +- .../FieldValuePart/LanguageFieldValuePart.php | 6 +- src/Header/AcceptCharset.php | 4 +- src/Header/AcceptEncoding.php | 4 +- src/Header/AcceptLanguage.php | 4 +- src/Header/CacheControl.php | 3 +- src/Header/ContentSecurityPolicy.php | 2 +- src/Header/ContentType.php | 24 +- src/Header/Cookie.php | 17 +- src/Header/HeaderValue.php | 6 +- src/Header/RetryAfter.php | 2 +- src/Header/SetCookie.php | 82 +++--- src/Headers.php | 31 +- src/PhpEnvironment/RemoteAddress.php | 4 +- src/PhpEnvironment/Request.php | 38 ++- src/PhpEnvironment/Response.php | 1 - src/Request.php | 33 ++- src/Response.php | 50 +++- src/Response/Stream.php | 27 +- test/ClientTest.php | 270 +++++++++++++++++- test/CookiesTest.php | 43 +++ .../UnexpectedValueExceptionTest.php | 25 ++ test/Header/SetCookieTest.php | 4 +- test/HeadersTest.php | 6 + test/ResponseTest.php | 46 +++ 45 files changed, 1028 insertions(+), 319 deletions(-) create mode 100644 phpstan.neon.dist create mode 100644 src/Client/Adapter/Exception/UnexpectedValueException.php create mode 100644 src/Client/Exception/UnexpectedValueException.php create mode 100644 src/Exception/UnexpectedValueException.php create mode 100644 test/Exception/UnexpectedValueExceptionTest.php 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..e3832d532b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ All notable changes to this project will be documented in this file, in reverse ### Deprecated -- Nothing. +- Nothing ### Removed @@ -22,7 +22,8 @@ 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)`. ## 2.11.1 - TBD diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000000..76caeded7b --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,18 @@ +parameters: + level: max + + excludes_analyse: + - %currentWorkingDirectory%/docs/* + - %currentWorkingDirectory%/test/* + - %currentWorkingDirectory%/vendor/* + + universalObjectCratesClasses: + - Zend\Http\Header\Accept\FieldValuePart\AbstractFieldValuePart + + ignoreErrors: + - "#Casting to [a-z]+ something that's already [a-z]+#" + - "#Negated boolean is always false#" + - "#Result of && is always false#" + - "#Strict comparison using === between 8 and 4 will always evaluate to false#" + - '#Parameter \#1 $port of method Zend\\Uri\\Uri::setPort() expects int, int|null given#' + - '#Access to an undefined property object::\$params#' 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..ed0cd04e4f 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,14 @@ 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 (! preg_match('/' . preg_quote($lastHost, '/') . '$/i', $nextHost)) { $this->clearAuth(); } @@ -412,7 +422,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 +431,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 +500,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 +512,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 +544,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 +559,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 +690,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 +716,7 @@ public function setStream($streamfile = true) /** * Get status of streaming for received data - * @return bool|string + * @return bool|resource|string */ public function getStream() { @@ -729,7 +742,11 @@ protected function openTempStream() $this->streamName = tempnam( isset($this->config['streamtmpdir']) ? $this->config['streamtmpdir'] : sys_get_temp_dir(), Client::class - ); + ) ?: null; + + if (null === $this->streamName) { + throw new RuntimeException('Unable to create temporary name for stream'); + } } ErrorHandler::start(); @@ -838,6 +855,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 +874,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); } @@ -925,7 +949,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 +961,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 +983,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 +1024,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 +1070,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 +1167,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 +1208,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 +1227,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 +1235,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 +1244,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 +1258,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 +1287,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 +1314,17 @@ 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')); + $this->setEncType($this->getHeader('Content-Type') ?: null); } // If we have POST parameters or files, encode and add them to the body @@ -1332,14 +1386,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 +1469,7 @@ protected function flattenParametersArray($parray, $prefix = null) $key = $prefix . sprintf('[%s]', $name); } } else { - $key = $name; + $key = (string) $name; } if (is_array($value)) { @@ -1440,19 +1496,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 +1522,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..15943f6fa9 100644 --- a/src/Client/Adapter/Curl.php +++ b/src/Client/Adapter/Curl.php @@ -42,7 +42,7 @@ class Curl implements HttpAdapter, StreamInterface /** * The curl session handle * - * @var resource|null + * @var null|resource */ protected $curl; @@ -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,12 +274,12 @@ 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 float $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. @@ -455,16 +458,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,6 +475,14 @@ 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); @@ -519,7 +525,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 +548,7 @@ public function close() /** * Get cUrl Handle * - * @return resource + * @return null|resource */ public function getHandle() { @@ -552,7 +558,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..0cec9ddb99 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/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..5af933d298 100644 --- a/src/Cookies.php +++ b/src/Cookies.php @@ -8,6 +8,7 @@ namespace Zend\Http; use ArrayIterator; +use Zend\Http\Exception\InvalidArgumentException; use Zend\Http\Header\SetCookie; use Zend\Uri; @@ -71,7 +72,7 @@ class Cookies extends Headers /** * @static * @throws Exception\RuntimeException - * @param $string + * @param string $string * @return void */ public static function fromString($string) @@ -87,13 +88,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 +121,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(); + $cookieHdrs = $headers->get('Set-Cookie'); - if (is_array($cookieHdrs) || $cookieHdrs instanceof ArrayIterator) { + if ($cookieHdrs instanceof ArrayIterator) { foreach ($cookieHdrs as $cookie) { - $this->addCookie($cookie, $refUri); + $this->addCookie($cookie); } } elseif (is_string($cookieHdrs)) { - $this->addCookie($cookieHdrs, $refUri); + $this->addCookie($cookieHdrs); } } @@ -197,7 +200,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 +216,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 +224,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 +233,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( @@ -264,21 +268,22 @@ protected function _flattenCookiesArray($ptr, $retAs = self::COOKIE_OBJECT) } } 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; } /** @@ -316,7 +321,11 @@ protected function _matchPath($domains, $path) $ret = []; foreach ($domains as $dom => $pathsArray) { - foreach (array_keys($pathsArray) as $cpath) { + $keys = array_keys($pathsArray); + foreach ($keys as $cpath) { + if (! \is_string($cpath)) { + throw new InvalidArgumentException('Domains is not a valid array'); + } if (SetCookie::matchCookiePath($cpath, $path)) { if (! isset($ret[$dom])) { $ret[$dom] = []; diff --git a/src/Exception/UnexpectedValueException.php b/src/Exception/UnexpectedValueException.php new file mode 100644 index 0000000000..99f44d9d3d --- /dev/null +++ b/src/Exception/UnexpectedValueException.php @@ -0,0 +1,26 @@ +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 +331,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 +377,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..c3cd1b169a 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,7 +77,7 @@ public function getTypeString() */ public function getPriority() { - return (float) $this->getInternalValues()->priority; + return (float) $this->priority; } /** @@ -85,7 +85,7 @@ public function getPriority() */ public function getParams() { - return (object) $this->getInternalValues()->params; + return (object) $this->params; } /** @@ -93,7 +93,16 @@ public function getParams() */ public function getRaw() { - return $this->getInternalValues()->raw; + return $this->raw; + } + + /** + * @param string $key + * @return mixed + */ + public function getInternalType($key) + { + return $this->getInternalValues()->$key; } /** 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..c2d889efb6 100644 --- a/src/Header/ContentType.php +++ b/src/Header/ContentType.php @@ -16,7 +16,7 @@ class ContentType implements HeaderInterface { /** - * @var string + * @var null|string */ protected $mediaType; @@ -26,7 +26,7 @@ class ContentType implements HeaderInterface protected $parameters = []; /** - * @var string + * @var null|string */ protected $value; @@ -49,6 +49,7 @@ public static function fromString($headerLine) } $parts = explode(';', $value); + /** @var string $mediaType */ $mediaType = array_shift($parts); $header = new static($value, trim($mediaType)); @@ -168,7 +169,7 @@ public function setMediaType($mediaType) /** * Get the media type * - * @return string + * @return null|string */ public function getMediaType() { @@ -238,7 +239,7 @@ protected function assembleValue() { $mediaType = $this->getMediaType(); if (empty($this->parameters)) { - return $mediaType; + return $mediaType ?: ''; } $parameters = []; @@ -275,7 +276,7 @@ function (&$value) { * - subtype * - format * - * @param string $string + * @param null|string $string * @return stdClass */ protected function getMediaTypeObjectFromString($string) @@ -287,15 +288,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, '+')) { @@ -356,17 +360,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 stdClass $right + * @param stdClass $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..8f72a35b44 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', @@ -55,10 +62,14 @@ public static function fromString($headerLine) $nvPairs = preg_split('#;\s*#', $value); + if (false === $nvPairs) { + throw new Exception\RuntimeException('Malformed Cookie header found'); + } + $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; @@ -78,7 +89,7 @@ public function __construct(array $array = []) /** * @param bool $encodeValue * - * @return $this + * @return self */ public function setEncodeValue($encodeValue) { @@ -104,7 +115,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/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..2838ef8fc3 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) @@ -258,7 +258,7 @@ public function __construct( ->setExpires($expires) ->setPath($path) ->setSecure($secure) - ->setHttpOnly($httponly) + ->setHttponly($httponly) ->setSameSite($sameSite); } @@ -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 . '"'; } @@ -332,7 +334,7 @@ public function getFieldValue() $fieldValue .= '; Secure'; } - if ($this->isHttponly()) { + if ($this->isHttpOnly()) { $fieldValue .= '; HttpOnly'; } @@ -345,7 +347,7 @@ public function getFieldValue() } /** - * @param string|null $name + * @param null|string|null $name * @return $this * @throws Exception\InvalidArgumentException */ @@ -357,7 +359,7 @@ public function setName($name) } /** - * @return string|null + * @return null|string|null */ public function getName() { @@ -365,7 +367,7 @@ public function getName() } /** - * @param string|null $value + * @param null|string|null $value * @return $this */ public function setValue($value) @@ -375,7 +377,7 @@ public function setValue($value) } /** - * @return string|null + * @return null|string|null */ public function getValue() { @@ -383,7 +385,7 @@ public function getValue() } /** - * @param int|null $version + * @param null|int|null $version * @return $this * @throws Exception\InvalidArgumentException */ @@ -397,7 +399,7 @@ public function setVersion($version) } /** - * @return int|null + * @return null|int|null */ public function getVersion() { @@ -405,7 +407,7 @@ public function getVersion() } /** - * @param int $maxAge + * @param null|int $maxAge * @return $this */ public function setMaxAge($maxAge) @@ -419,7 +421,7 @@ public function setMaxAge($maxAge) } /** - * @return int|null + * @return null|int|null */ public function getMaxAge() { @@ -427,7 +429,7 @@ public function getMaxAge() } /** - * @param int|string|DateTime|null $expires + * @param null|int|string|DateTime|null $expires * @return $this * @throws Exception\InvalidArgumentException */ @@ -467,7 +469,7 @@ public function setExpires($expires) /** * @param bool $inSeconds - * @return int|string|null + * @return null|int|string|null */ public function getExpires($inSeconds = false) { @@ -481,7 +483,7 @@ public function getExpires($inSeconds = false) } /** - * @param string|null $domain + * @param null|string|null $domain * @return $this */ public function setDomain($domain) @@ -492,7 +494,7 @@ public function setDomain($domain) } /** - * @return string|null + * @return null|string|null */ public function getDomain() { @@ -500,7 +502,7 @@ public function getDomain() } /** - * @param string|null $path + * @param null|string|null $path * @return $this */ public function setPath($path) @@ -511,7 +513,7 @@ public function setPath($path) } /** - * @return string|null + * @return null|string|null */ public function getPath() { @@ -519,7 +521,7 @@ public function getPath() } /** - * @param bool|null $secure + * @param null|bool|null $secure * @return $this */ public function setSecure($secure) @@ -544,7 +546,7 @@ public function setQuoteFieldValue($quotedValue) } /** - * @return bool|null + * @return null|bool|null */ public function isSecure() { @@ -552,7 +554,7 @@ public function isSecure() } /** - * @param bool|null $httponly + * @param null|bool|null $httponly * @return $this */ public function setHttponly($httponly) @@ -565,9 +567,9 @@ public function setHttponly($httponly) } /** - * @return bool|null + * @return null|bool|null */ - public function isHttponly() + public function isHttpOnly() { return $this->httponly; } @@ -577,7 +579,7 @@ public function isHttponly() * * Always returns false if the cookie is a session cookie (has no expiry time) * - * @param int|null $now Timestamp to consider as "now" + * @param null|int|null $now Timestamp to consider as "now" * @return bool */ public function isExpired($now = null) @@ -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..4e69ebceed 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() { @@ -143,6 +144,7 @@ protected function getIpAddressFromProxy() // not know if it is a proxy server, or a client. As such, we treat it // as the originating IP. // @see http://en.wikipedia.org/wiki/X-Forwarded-For + /** @var string $ip */ $ip = array_pop($ips); return $ip; } diff --git a/src/PhpEnvironment/Request.php b/src/PhpEnvironment/Request.php index 4101af1752..02c5469d5e 100644 --- a/src/PhpEnvironment/Request.php +++ b/src/PhpEnvironment/Request.php @@ -7,7 +7,10 @@ namespace Zend\Http\PhpEnvironment; +use Zend\Http\Exception\RuntimeException; 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 +96,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 +109,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; } @@ -215,7 +220,7 @@ public function setServer(ParametersInterface $server) } // set headers - $headers = []; + $headersArray = []; foreach ($server as $key => $value) { if ($value || (! is_array($value) && strlen($value))) { @@ -225,15 +230,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 +273,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 +318,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 +457,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 +540,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..1241b81fe6 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( @@ -128,6 +130,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; @@ -318,7 +321,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 +374,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 +398,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 +513,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 +527,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 +549,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..5cc8970a59 100644 --- a/src/Response.php +++ b/src/Response.php @@ -7,7 +7,10 @@ namespace Zend\Http; +use ArrayIterator; use Zend\Http\Exception\RuntimeException; +use Zend\Http\Exception\UnexpectedValueException; +use Zend\Http\Header\HeaderInterface; use Zend\Stdlib\ErrorHandler; use Zend\Stdlib\ResponseInterface; @@ -197,6 +200,7 @@ public static function fromString($string) $lines = explode("\n", $string); } + /** @var string $firstLine */ $firstLine = array_shift($lines); $response = new static(); @@ -208,6 +212,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 RuntimeException('Invalid response content'); + } $response->parseStatusLine($next); } @@ -273,11 +280,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 +371,7 @@ public function setReasonPhrase($reasonPhrase) /** * Get HTTP status message * - * @return string + * @return null|string */ public function getReasonPhrase() { @@ -380,14 +390,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 +565,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 +591,10 @@ protected function decodeGzip($body) ); } - if ($body === '' - || ($this->getHeaders()->has('content-length') - && (int) $this->getHeaders()->get('content-length')->getFieldValue() === 0) + $contentLengthHeader = $body === '' + || ($this->getHeaders()->get('content-length'); + if ($contentLengthHeader instanceof Header\ContentLength + && (int) $contentLengthHeader->getFieldValue() === 0) ) { return ''; } @@ -588,7 +602,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 +629,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 +649,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..3e90945378 100644 --- a/test/ClientTest.php +++ b/test/ClientTest.php @@ -24,10 +24,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 +139,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 +201,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 +225,7 @@ public function testCanOverrideArgSeparator() public function testClientUsesAcceptEncodingHeaderFromRequestObject() { - $client = new Client(); + $client = new Client('http://foo.com'); $client->setAdapter(Test::class); @@ -266,6 +299,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 +588,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 +610,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 +621,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'); @@ -541,7 +645,8 @@ public function uriDataProvider() */ public function testUriCorrectlyDeterminesWhetherOrNotItIsAValidRelativeUri($uri, $isValidRelativeURI) { - $client = new Client($uri); + $client = new Client('http://www.domain.com'); + $client->setUri($uri); $this->assertSame($isValidRelativeURI, $client->getUri()->isValidRelative()); $client->setAdapter(Test::class); @@ -637,4 +742,163 @@ 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 testClientThrowsAnErrorWhenUnableToCreateStream() + { + \putenv('TMPDIR=/__________foooo'); + \error_reporting(\E_ALL ^ \E_STRICT ^ \E_NOTICE); + $this->expectException(HttpException\RuntimeException::class); + $this->expectExceptionMessage('Unable to create temporary name for stream'); + + $options = [ + 'outputstream' => true, + ]; + $client = new Client('http://foo.com', $options); + + $adapter = $this->prophesize(Client\Adapter\AdapterInterface::class); + $adapter->willImplement(Client\Adapter\StreamInterface::class); + + $client->setAdapter($adapter->reveal()); + + $request = $client->getRequest(); + + $acceptEncodingHeader = new AcceptEncoding(); + $acceptEncodingHeader->addEncoding('foo', 1); + $request->getHeaders()->addHeader($acceptEncodingHeader); + + $client->send(); + } } 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..c4806c6ae5 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' From 4ec3898ee47e5005e629c4f109a839fdd05a9e4b Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Wed, 20 Feb 2019 00:28:22 +0100 Subject: [PATCH 02/21] phpstan fixes --- phpstan.neon.dist | 39 ++++++++++++++++++++++++++----- src/Client.php | 15 ++++++++---- src/Client/Adapter/Curl.php | 13 +++++++---- src/Cookies.php | 7 +++++- src/Exception/LogicException.php | 13 +++++++++++ src/Header/AbstractAccept.php | 2 +- src/Header/GenericMultiHeader.php | 4 ++++ src/PhpEnvironment/Request.php | 2 +- src/Request.php | 14 +++++++++++ test/ClientTest.php | 21 +++++------------ 10 files changed, 97 insertions(+), 33 deletions(-) create mode 100644 src/Exception/LogicException.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 76caeded7b..bbd409fa5b 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -10,9 +10,36 @@ parameters: - Zend\Http\Header\Accept\FieldValuePart\AbstractFieldValuePart ignoreErrors: - - "#Casting to [a-z]+ something that's already [a-z]+#" - - "#Negated boolean is always false#" - - "#Result of && is always false#" - - "#Strict comparison using === between 8 and 4 will always evaluate to false#" - - '#Parameter \#1 $port of method Zend\\Uri\\Uri::setPort() expects int, int|null given#' - - '#Access to an undefined property object::\$params#' + - + 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 diff --git a/src/Client.php b/src/Client.php index ed0cd04e4f..656fcc26e5 100644 --- a/src/Client.php +++ b/src/Client.php @@ -331,6 +331,10 @@ public function setUri($uri) // 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(); } @@ -809,7 +813,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 @@ -838,7 +842,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 @@ -917,7 +921,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); @@ -1324,7 +1328,8 @@ protected function prepareBody() $this->setEncType(self::ENC_FORMDATA); } } else { - $this->setEncType($this->getHeader('Content-Type') ?: null); + $contentType = $this->getHeader('Content-Type'); + $this->setEncType(\is_string($contentType) && ! empty($contentType) ? $contentType: null); } // If we have POST parameters or files, encode and add them to the body @@ -1353,7 +1358,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', diff --git a/src/Client/Adapter/Curl.php b/src/Client/Adapter/Curl.php index 15943f6fa9..cf3f0747ae 100644 --- a/src/Client/Adapter/Curl.php +++ b/src/Client/Adapter/Curl.php @@ -276,7 +276,7 @@ public function connect($host, $port = 80, $secure = false) * * @param string $method * @param \Zend\Uri\Uri $uri - * @param float $httpVersion + * @param string $httpVersion * @param array $headers * @param string|resource $body * @return string $request @@ -286,8 +286,10 @@ public function connect($host, $port = 80, $secure = false) * @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'); @@ -383,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); @@ -489,12 +491,14 @@ public function write($method, $uri, $httpVersion = 1.1, $headers = [], $body = // 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); } @@ -511,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; } diff --git a/src/Cookies.php b/src/Cookies.php index 5af933d298..a04a04a0bd 100644 --- a/src/Cookies.php +++ b/src/Cookies.php @@ -9,6 +9,7 @@ use ArrayIterator; use Zend\Http\Exception\InvalidArgumentException; +use Zend\Http\Exception\LogicException; use Zend\Http\Header\SetCookie; use Zend\Uri; @@ -264,7 +265,11 @@ 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; 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 @@ +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'])) { diff --git a/src/Request.php b/src/Request.php index 1241b81fe6..bb7d70364a 100644 --- a/src/Request.php +++ b/src/Request.php @@ -113,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); @@ -216,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; diff --git a/test/ClientTest.php b/test/ClientTest.php index 3e90945378..636de3d0c0 100644 --- a/test/ClientTest.php +++ b/test/ClientTest.php @@ -632,26 +632,17 @@ 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($uri); - $this->assertSame($isValidRelativeURI, $client->getUri()->isValidRelative()); - - $client->setAdapter(Test::class); - $client->send(); - $this->assertSame($isValidRelativeURI, $client->getUri()->isValidRelative()); + $client->setUri('/example'); } public function portChangeDataProvider() From 2ee0df0916cec51b2da92c18aa82d840d115067a Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Wed, 20 Feb 2019 00:33:22 +0100 Subject: [PATCH 03/21] Updated CHANGELOG --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3832d532b..7c5235db41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,12 @@ All notable changes to this project will be documented in this file, in reverse ### Changed -- Nothing. +- 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 -- Nothing +- Nothing. ### Removed @@ -24,6 +25,7 @@ All notable changes to this project will be documented in this file, in reverse - 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 From aae9e1ee9cd706a41a6d6086a901a57df17da67c Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Wed, 20 Feb 2019 01:06:14 +0100 Subject: [PATCH 04/21] Replaced @return __CLASS__ with @return self --- src/Client/Adapter/Test.php | 2 +- src/Header/SetCookie.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) 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/Header/SetCookie.php b/src/Header/SetCookie.php index 2838ef8fc3..96b27e5086 100644 --- a/src/Header/SetCookie.php +++ b/src/Header/SetCookie.php @@ -367,7 +367,7 @@ public function getName() } /** - * @param null|string|null $value + * @param null|string $value * @return $this */ public function setValue($value) @@ -483,7 +483,7 @@ public function getExpires($inSeconds = false) } /** - * @param null|string|null $domain + * @param null|string $domain * @return $this */ public function setDomain($domain) @@ -502,7 +502,7 @@ public function getDomain() } /** - * @param null|string|null $path + * @param null|string $path * @return $this */ public function setPath($path) @@ -521,7 +521,7 @@ public function getPath() } /** - * @param null|bool|null $secure + * @param null|bool $secure * @return $this */ public function setSecure($secure) @@ -554,7 +554,7 @@ public function isSecure() } /** - * @param null|bool|null $httponly + * @param null|bool $httponly * @return $this */ public function setHttponly($httponly) From a29e1680437201117398a3de6f210427e180f036 Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Wed, 20 Feb 2019 01:07:30 +0100 Subject: [PATCH 05/21] Replaced stdClass with object in docs --- phpstan.neon.dist | 1 + src/Header/AbstractAccept.php | 2 -- .../Accept/FieldValuePart/AbstractFieldValuePart.php | 2 +- src/Header/ContentType.php | 12 +++++------- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index bbd409fa5b..53728fdd73 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -43,3 +43,4 @@ parameters: - 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/Header/AbstractAccept.php b/src/Header/AbstractAccept.php index e59cbdfc46..1288f8e711 100644 --- a/src/Header/AbstractAccept.php +++ b/src/Header/AbstractAccept.php @@ -7,8 +7,6 @@ namespace Zend\Http\Header; -use stdClass; - /** * Abstract Accept Header * diff --git a/src/Header/Accept/FieldValuePart/AbstractFieldValuePart.php b/src/Header/Accept/FieldValuePart/AbstractFieldValuePart.php index c3cd1b169a..f1601e2319 100644 --- a/src/Header/Accept/FieldValuePart/AbstractFieldValuePart.php +++ b/src/Header/Accept/FieldValuePart/AbstractFieldValuePart.php @@ -81,7 +81,7 @@ public function getPriority() } /** - * @return \stdClass $params + * @return object $params */ public function getParams() { diff --git a/src/Header/ContentType.php b/src/Header/ContentType.php index c2d889efb6..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 @@ -277,7 +275,7 @@ function (&$value) { * - format * * @param null|string $string - * @return stdClass + * @return object */ protected function getMediaTypeObjectFromString($string) { @@ -320,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) @@ -360,8 +358,8 @@ protected function validateSubtype($right, $left) * * Validate that the right side format matches what the left side defines. * - * @param stdClass $right - * @param stdClass $left + * @param object $right + * @param object $left * @return bool */ protected function validateFormat($right, $left) From 4c8c637311c0d83b7ec0c49ea38bc499cee7b4cb Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Wed, 20 Feb 2019 01:12:36 +0100 Subject: [PATCH 06/21] Changed inline doc type --- src/PhpEnvironment/RemoteAddress.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpEnvironment/RemoteAddress.php b/src/PhpEnvironment/RemoteAddress.php index 4e69ebceed..1d8dde2327 100644 --- a/src/PhpEnvironment/RemoteAddress.php +++ b/src/PhpEnvironment/RemoteAddress.php @@ -132,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? @@ -144,7 +145,6 @@ protected function getIpAddressFromProxy() // not know if it is a proxy server, or a client. As such, we treat it // as the originating IP. // @see http://en.wikipedia.org/wiki/X-Forwarded-For - /** @var string $ip */ $ip = array_pop($ips); return $ip; } From c638944cac8bf6e09e36e034a0fb55cdbd9fb440 Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Wed, 20 Feb 2019 01:30:31 +0100 Subject: [PATCH 07/21] Added tests for URIs with no host --- test/ClientTest.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/ClientTest.php b/test/ClientTest.php index 636de3d0c0..b9183a6150 100644 --- a/test/ClientTest.php +++ b/test/ClientTest.php @@ -645,6 +645,19 @@ public function testRelativeUriIsNotAllowed() $client->setUri('/example'); } + public function testRelativeUriIsNotAllowedSendingRequest() + { + $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($request); + } + public function portChangeDataProvider() { return [ From d52d58cecba46544c797f6c55e5ddad22d193c8f Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Wed, 20 Feb 2019 01:53:33 +0100 Subject: [PATCH 08/21] Fixed set default encType --- src/Client.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index 656fcc26e5..deab86a2d8 100644 --- a/src/Client.php +++ b/src/Client.php @@ -1329,7 +1329,7 @@ protected function prepareBody() } } else { $contentType = $this->getHeader('Content-Type'); - $this->setEncType(\is_string($contentType) && ! empty($contentType) ? $contentType: null); + $this->setEncType(\is_string($contentType) ? $contentType : ''); } // If we have POST parameters or files, encode and add them to the body From 07e13df81a034f65313d481bf8a849360a024191 Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Wed, 20 Feb 2019 02:00:28 +0100 Subject: [PATCH 09/21] Added tests for doRequest with URI with no host --- test/ClientTest.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/ClientTest.php b/test/ClientTest.php index b9183a6150..99eca54c98 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; @@ -905,4 +906,18 @@ public function testClientThrowsAnErrorWhenUnableToCreateStream() $client->send(); } + + 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']); + } } From 1de59dde9533102b087e9933cc5be45bcc84cf67 Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Wed, 20 Feb 2019 02:00:42 +0100 Subject: [PATCH 10/21] Assignment align --- src/Client/Adapter/Proxy.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Client/Adapter/Proxy.php b/src/Client/Adapter/Proxy.php index f20d99b1c4..b9e1e68a3b 100644 --- a/src/Client/Adapter/Proxy.php +++ b/src/Client/Adapter/Proxy.php @@ -159,9 +159,9 @@ public function write($method, $uri, $httpVer = '1.1', $headers = [], $body = '' ); } - $host = $uri->getHost(); + $host = $uri->getHost(); $scheme = $uri->getScheme(); - $port = $uri->getPort(); + $port = $uri->getPort(); if (null === $host || null === $scheme || null === $port) { throw new AdapterException\InvalidArgumentException( From 81810769cb03465eb5a19b267c695af891b637d3 Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Wed, 20 Feb 2019 02:05:29 +0100 Subject: [PATCH 11/21] Removed optimized use of core functions --- src/Client.php | 4 ++-- src/Client/Adapter/Curl.php | 2 +- src/Client/Adapter/Socket.php | 2 +- src/Cookies.php | 4 ++-- src/Exception/UnexpectedValueException.php | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Client.php b/src/Client.php index deab86a2d8..759b0fa4db 100644 --- a/src/Client.php +++ b/src/Client.php @@ -1028,7 +1028,7 @@ public function send(Request $request = null) // Get the cookies from response (if any) $setCookies = $response->getCookie(); - if (! \is_bool($setCookies) && ! empty($setCookies)) { + if (! is_bool($setCookies) && ! empty($setCookies)) { $this->addCookie($setCookies); } @@ -1329,7 +1329,7 @@ protected function prepareBody() } } else { $contentType = $this->getHeader('Content-Type'); - $this->setEncType(\is_string($contentType) ? $contentType : ''); + $this->setEncType(is_string($contentType) ? $contentType : ''); } // If we have POST parameters or files, encode and add them to the body diff --git a/src/Client/Adapter/Curl.php b/src/Client/Adapter/Curl.php index cf3f0747ae..deccb364e3 100644 --- a/src/Client/Adapter/Curl.php +++ b/src/Client/Adapter/Curl.php @@ -518,7 +518,7 @@ public function write($method, $uri, $httpVersion = '1.1', $headers = [], $body $again = false; - if (\is_array($parts) && 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; } diff --git a/src/Client/Adapter/Socket.php b/src/Client/Adapter/Socket.php index 0cec9ddb99..7cc35cf97f 100644 --- a/src/Client/Adapter/Socket.php +++ b/src/Client/Adapter/Socket.php @@ -288,7 +288,7 @@ public function connect($host, $port = 80, $secure = false) ) ?: null; $error = ErrorHandler::stop(); - if (! \is_resource($this->socket)) { + if (! is_resource($this->socket)) { $this->close(); throw new AdapterException\RuntimeException( sprintf( diff --git a/src/Cookies.php b/src/Cookies.php index a04a04a0bd..cf59b71d5a 100644 --- a/src/Cookies.php +++ b/src/Cookies.php @@ -266,7 +266,7 @@ protected function _flattenCookiesArray($ptr, $retAs = self::COOKIE_OBJECT) $ret .= $this->_flattenCookiesArray($item, $retAs); } else { $flatten = $this->_flattenCookiesArray($item, $retAs); - if (!\is_array($ret) || ! \is_array($flatten)) { + if (!is_array($ret) || ! is_array($flatten)) { throw new LogicException('Flatten cookies is not an array'); } $ret = array_merge($ret, $flatten); @@ -328,7 +328,7 @@ protected function _matchPath($domains, $path) foreach ($domains as $dom => $pathsArray) { $keys = array_keys($pathsArray); foreach ($keys as $cpath) { - if (! \is_string($cpath)) { + if (! is_string($cpath)) { throw new InvalidArgumentException('Domains is not a valid array'); } if (SetCookie::matchCookiePath($cpath, $path)) { diff --git a/src/Exception/UnexpectedValueException.php b/src/Exception/UnexpectedValueException.php index 99f44d9d3d..293eb94862 100644 --- a/src/Exception/UnexpectedValueException.php +++ b/src/Exception/UnexpectedValueException.php @@ -20,7 +20,7 @@ public static function unexpectedType($expected, $actual) return new static(sprintf( 'Expected %s. %s given', $expected, - \is_object($actual) ? \get_class($actual) : \gettype($actual) + is_object($actual) ? get_class($actual) : gettype($actual) )); } } From 4847820a266dd564069a62c03e89dfe0e04a9e3c Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Wed, 20 Feb 2019 02:14:14 +0100 Subject: [PATCH 12/21] Replaced variable names --- src/Cookies.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Cookies.php b/src/Cookies.php index cf59b71d5a..1fd6d2e12d 100644 --- a/src/Cookies.php +++ b/src/Cookies.php @@ -124,14 +124,14 @@ public function addCookiesFromResponse(Response $response, $refUri) { /** @var Headers $headers */ $headers = $response->getHeaders(); - $cookieHdrs = $headers->get('Set-Cookie'); + $cookieHeaders = $headers->get('Set-Cookie'); - if ($cookieHdrs instanceof ArrayIterator) { - foreach ($cookieHdrs as $cookie) { + if ($cookieHeaders instanceof ArrayIterator) { + foreach ($cookieHeaders as $cookie) { $this->addCookie($cookie); } - } elseif (is_string($cookieHdrs)) { - $this->addCookie($cookieHdrs); + } elseif (is_string($cookieHeaders)) { + $this->addCookie($cookieHeaders); } } From c9fd13b41c96ed10c78d66213ef3412a6e0da4d4 Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Wed, 20 Feb 2019 02:23:36 +0100 Subject: [PATCH 13/21] Pass $refUri adding cookies to avoid BC break with extended classes --- src/Cookies.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Cookies.php b/src/Cookies.php index 1fd6d2e12d..87c52e0a33 100644 --- a/src/Cookies.php +++ b/src/Cookies.php @@ -128,10 +128,10 @@ public function addCookiesFromResponse(Response $response, $refUri) if ($cookieHeaders instanceof ArrayIterator) { foreach ($cookieHeaders as $cookie) { - $this->addCookie($cookie); + $this->addCookie($cookie, $refUri); } } elseif (is_string($cookieHeaders)) { - $this->addCookie($cookieHeaders); + $this->addCookie($cookieHeaders, $refUri); } } From 837bdd94cdc3240d0a071f700d96b5d1537985a4 Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Wed, 20 Feb 2019 02:32:05 +0100 Subject: [PATCH 14/21] Fixed cookies class --- src/Cookies.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Cookies.php b/src/Cookies.php index 87c52e0a33..fed16c7ab4 100644 --- a/src/Cookies.php +++ b/src/Cookies.php @@ -217,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) { @@ -315,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 */ @@ -326,11 +326,9 @@ protected function _matchPath($domains, $path) $ret = []; foreach ($domains as $dom => $pathsArray) { + /** @var string[] $keys */ $keys = array_keys($pathsArray); foreach ($keys as $cpath) { - if (! is_string($cpath)) { - throw new InvalidArgumentException('Domains is not a valid array'); - } if (SetCookie::matchCookiePath($cpath, $path)) { if (! isset($ret[$dom])) { $ret[$dom] = []; From b90d014cd966e94dcf69a4f97733fa456e477fbc Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Wed, 20 Feb 2019 02:33:20 +0100 Subject: [PATCH 15/21] Removed unused method --- .../Accept/FieldValuePart/AbstractFieldValuePart.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Header/Accept/FieldValuePart/AbstractFieldValuePart.php b/src/Header/Accept/FieldValuePart/AbstractFieldValuePart.php index f1601e2319..bce07c447d 100644 --- a/src/Header/Accept/FieldValuePart/AbstractFieldValuePart.php +++ b/src/Header/Accept/FieldValuePart/AbstractFieldValuePart.php @@ -96,15 +96,6 @@ public function getRaw() return $this->raw; } - /** - * @param string $key - * @return mixed - */ - public function getInternalType($key) - { - return $this->getInternalValues()->$key; - } - /** * @param mixed $key * @return mixed From 2b156c30c8367a25d25a58ecadb25123c6e4fab1 Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Wed, 20 Feb 2019 02:47:14 +0100 Subject: [PATCH 16/21] Other fixes and tests --- CHANGELOG.md | 2 ++ src/Header/Cookie.php | 5 +---- src/PhpEnvironment/Request.php | 1 - src/Response.php | 6 ++---- test/ResponseTest.php | 8 ++++++++ 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c5235db41..ad83663308 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ All notable changes to this project will be documented in this file, in reverse ### Changed +- `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. diff --git a/src/Header/Cookie.php b/src/Header/Cookie.php index 8f72a35b44..7d64ef7366 100644 --- a/src/Header/Cookie.php +++ b/src/Header/Cookie.php @@ -60,12 +60,9 @@ public static function fromString($headerLine) throw new Exception\InvalidArgumentException('Invalid header line for Server string: "' . $name . '"'); } + /** @var string[] $nvPairs */ $nvPairs = preg_split('#;\s*#', $value); - if (false === $nvPairs) { - throw new Exception\RuntimeException('Malformed Cookie header found'); - } - $arrayInfo = []; foreach ($nvPairs as $nvPair) { $parts = explode('=', $nvPair, 2); diff --git a/src/PhpEnvironment/Request.php b/src/PhpEnvironment/Request.php index 3c85ed4e08..b929a5d1dc 100644 --- a/src/PhpEnvironment/Request.php +++ b/src/PhpEnvironment/Request.php @@ -7,7 +7,6 @@ namespace Zend\Http\PhpEnvironment; -use Zend\Http\Exception\RuntimeException; use Zend\Http\Header\Cookie; use Zend\Http\Header\HeaderInterface; use Zend\Http\Headers; diff --git a/src/Response.php b/src/Response.php index 5cc8970a59..5777f86ece 100644 --- a/src/Response.php +++ b/src/Response.php @@ -7,9 +7,7 @@ namespace Zend\Http; -use ArrayIterator; -use Zend\Http\Exception\RuntimeException; -use Zend\Http\Exception\UnexpectedValueException; +use Zend\Http\Exception\InvalidArgumentException; use Zend\Http\Header\HeaderInterface; use Zend\Stdlib\ErrorHandler; use Zend\Stdlib\ResponseInterface; @@ -213,7 +211,7 @@ public static function fromString($string) $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 RuntimeException('Invalid response content'); + throw new Exception\InvalidArgumentException('Invalid response content'); } $response->parseStatusLine($next); } diff --git a/test/ResponseTest.php b/test/ResponseTest.php index c4806c6ae5..26008eff65 100644 --- a/test/ResponseTest.php +++ b/test/ResponseTest.php @@ -678,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 * From 88fda9d2288f502119783c20b3b21e271a21d66e Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Wed, 20 Feb 2019 02:54:55 +0100 Subject: [PATCH 17/21] Removed check of tempnam result --- src/Client.php | 5 +---- test/ClientTest.php | 26 -------------------------- 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/src/Client.php b/src/Client.php index 759b0fa4db..4654d05f39 100644 --- a/src/Client.php +++ b/src/Client.php @@ -743,14 +743,11 @@ protected function openTempStream() if (! is_string($this->streamName)) { // If name is not given, create temp name + /** @var string streamName */ $this->streamName = tempnam( isset($this->config['streamtmpdir']) ? $this->config['streamtmpdir'] : sys_get_temp_dir(), Client::class ) ?: null; - - if (null === $this->streamName) { - throw new RuntimeException('Unable to create temporary name for stream'); - } } ErrorHandler::start(); diff --git a/test/ClientTest.php b/test/ClientTest.php index 99eca54c98..6da7259de9 100644 --- a/test/ClientTest.php +++ b/test/ClientTest.php @@ -881,32 +881,6 @@ public function testClientRequestWillNotReuseCookiesFromDifferentDomain() $this->assertNotContains("\r\nCookie: foo=far; bar=far\r\n", $lastRawRequest); } - public function testClientThrowsAnErrorWhenUnableToCreateStream() - { - \putenv('TMPDIR=/__________foooo'); - \error_reporting(\E_ALL ^ \E_STRICT ^ \E_NOTICE); - $this->expectException(HttpException\RuntimeException::class); - $this->expectExceptionMessage('Unable to create temporary name for stream'); - - $options = [ - 'outputstream' => true, - ]; - $client = new Client('http://foo.com', $options); - - $adapter = $this->prophesize(Client\Adapter\AdapterInterface::class); - $adapter->willImplement(Client\Adapter\StreamInterface::class); - - $client->setAdapter($adapter->reveal()); - - $request = $client->getRequest(); - - $acceptEncodingHeader = new AcceptEncoding(); - $acceptEncodingHeader->addEncoding('foo', 1); - $request->getHeaders()->addHeader($acceptEncodingHeader); - - $client->send(); - } - public function testDoRequestWithNoHostUri() { $this->expectException(HttpException\InvalidArgumentException::class); From 9158ecf8cd68ba0a3e38a52c68bd531f13e03a72 Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Wed, 20 Feb 2019 03:00:16 +0100 Subject: [PATCH 18/21] PHPCS --- src/Cookies.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cookies.php b/src/Cookies.php index fed16c7ab4..dad5ecc56b 100644 --- a/src/Cookies.php +++ b/src/Cookies.php @@ -266,7 +266,7 @@ protected function _flattenCookiesArray($ptr, $retAs = self::COOKIE_OBJECT) $ret .= $this->_flattenCookiesArray($item, $retAs); } else { $flatten = $this->_flattenCookiesArray($item, $retAs); - if (!is_array($ret) || ! is_array($flatten)) { + if (! is_array($ret) || ! is_array($flatten)) { throw new LogicException('Flatten cookies is not an array'); } $ret = array_merge($ret, $flatten); From ccbbb5f74bd3bbb90f86993ddfb5d1ae0ea92848 Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Wed, 20 Feb 2019 10:19:13 +0100 Subject: [PATCH 19/21] Fixed variable type --- src/Client.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Client.php b/src/Client.php index 4654d05f39..5a0b43a0cd 100644 --- a/src/Client.php +++ b/src/Client.php @@ -743,21 +743,23 @@ protected function openTempStream() if (! is_string($this->streamName)) { // If name is not given, create temp name - /** @var string streamName */ $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; From ce2d9108affb50a90b89132328a7a21b061a0ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bundyra?= Date: Wed, 4 Dec 2019 22:47:33 +0000 Subject: [PATCH 20/21] Fix merge error --- src/Response.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Response.php b/src/Response.php index 5777f86ece..64a25a8466 100644 --- a/src/Response.php +++ b/src/Response.php @@ -589,10 +589,10 @@ protected function decodeGzip($body) ); } - $contentLengthHeader = $body === '' - || ($this->getHeaders()->get('content-length'); - if ($contentLengthHeader instanceof Header\ContentLength - && (int) $contentLengthHeader->getFieldValue() === 0) + $contentLengthHeader = $this->getHeaders()->get('content-length'); + if ($body === '' + || ($contentLengthHeader instanceof Header\ContentLength + && (int) $contentLengthHeader->getFieldValue() === 0) ) { return ''; } From ec10c86556c5b484bda315b6b6ff7ba53b27191d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Bundyra?= Date: Wed, 4 Dec 2019 22:58:01 +0000 Subject: [PATCH 21/21] Fixes other minor merge issues --- src/Client/Adapter/Curl.php | 2 +- src/Header/Cookie.php | 2 +- src/Header/SetCookie.php | 42 ++++++++++++++++++------------------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/Client/Adapter/Curl.php b/src/Client/Adapter/Curl.php index deccb364e3..7d331cfa4f 100644 --- a/src/Client/Adapter/Curl.php +++ b/src/Client/Adapter/Curl.php @@ -42,7 +42,7 @@ class Curl implements HttpAdapter, StreamInterface /** * The curl session handle * - * @var null|resource + * @var resource|null */ protected $curl; diff --git a/src/Header/Cookie.php b/src/Header/Cookie.php index 7d64ef7366..193397b384 100644 --- a/src/Header/Cookie.php +++ b/src/Header/Cookie.php @@ -86,7 +86,7 @@ public function __construct(array $array = []) /** * @param bool $encodeValue * - * @return self + * @return $this */ public function setEncodeValue($encodeValue) { diff --git a/src/Header/SetCookie.php b/src/Header/SetCookie.php index 96b27e5086..ffcbbc5bed 100644 --- a/src/Header/SetCookie.php +++ b/src/Header/SetCookie.php @@ -258,7 +258,7 @@ public function __construct( ->setExpires($expires) ->setPath($path) ->setSecure($secure) - ->setHttponly($httponly) + ->setHttpOnly($httponly) ->setSameSite($sameSite); } @@ -334,7 +334,7 @@ public function getFieldValue() $fieldValue .= '; Secure'; } - if ($this->isHttpOnly()) { + if ($this->isHttponly()) { $fieldValue .= '; HttpOnly'; } @@ -347,7 +347,7 @@ public function getFieldValue() } /** - * @param null|string|null $name + * @param string|null $name * @return $this * @throws Exception\InvalidArgumentException */ @@ -359,7 +359,7 @@ public function setName($name) } /** - * @return null|string|null + * @return string|null */ public function getName() { @@ -367,7 +367,7 @@ public function getName() } /** - * @param null|string $value + * @param string|null $value * @return $this */ public function setValue($value) @@ -377,7 +377,7 @@ public function setValue($value) } /** - * @return null|string|null + * @return string|null */ public function getValue() { @@ -385,7 +385,7 @@ public function getValue() } /** - * @param null|int|null $version + * @param int|null $version * @return $this * @throws Exception\InvalidArgumentException */ @@ -399,7 +399,7 @@ public function setVersion($version) } /** - * @return null|int|null + * @return int|null */ public function getVersion() { @@ -407,7 +407,7 @@ public function getVersion() } /** - * @param null|int $maxAge + * @param int|null $maxAge * @return $this */ public function setMaxAge($maxAge) @@ -421,7 +421,7 @@ public function setMaxAge($maxAge) } /** - * @return null|int|null + * @return int|null */ public function getMaxAge() { @@ -429,7 +429,7 @@ public function getMaxAge() } /** - * @param null|int|string|DateTime|null $expires + * @param int|string|DateTime|null $expires * @return $this * @throws Exception\InvalidArgumentException */ @@ -469,7 +469,7 @@ public function setExpires($expires) /** * @param bool $inSeconds - * @return null|int|string|null + * @return int|string|null */ public function getExpires($inSeconds = false) { @@ -483,7 +483,7 @@ public function getExpires($inSeconds = false) } /** - * @param null|string $domain + * @param string|null $domain * @return $this */ public function setDomain($domain) @@ -494,7 +494,7 @@ public function setDomain($domain) } /** - * @return null|string|null + * @return string|null */ public function getDomain() { @@ -502,7 +502,7 @@ public function getDomain() } /** - * @param null|string $path + * @param string|null $path * @return $this */ public function setPath($path) @@ -513,7 +513,7 @@ public function setPath($path) } /** - * @return null|string|null + * @return string|null */ public function getPath() { @@ -521,7 +521,7 @@ public function getPath() } /** - * @param null|bool $secure + * @param bool|null $secure * @return $this */ public function setSecure($secure) @@ -546,7 +546,7 @@ public function setQuoteFieldValue($quotedValue) } /** - * @return null|bool|null + * @return bool|null */ public function isSecure() { @@ -554,7 +554,7 @@ public function isSecure() } /** - * @param null|bool $httponly + * @param bool|null $httponly * @return $this */ public function setHttponly($httponly) @@ -567,7 +567,7 @@ public function setHttponly($httponly) } /** - * @return null|bool|null + * @return bool|null */ public function isHttpOnly() { @@ -579,7 +579,7 @@ public function isHttpOnly() * * Always returns false if the cookie is a session cookie (has no expiry time) * - * @param null|int|null $now Timestamp to consider as "now" + * @param int|null $now Timestamp to consider as "now" * @return bool */ public function isExpired($now = null)