From 1f74b01cc9abf7fbddff9545c0b28a8d02431294 Mon Sep 17 00:00:00 2001 From: Gordon Stratton Date: Mon, 10 Jun 2013 23:38:13 +0000 Subject: [PATCH 1/2] Refactor Zend\Version\Version to optionally use Zend\Http\Client With this update, we can perform version checks without relying on the environment having allow_url_fopen = 1. You can now optionally pass in a Zend\Http\Client instance. Tests are enhanced to run in a separate process to avoid problems during online tests where tests would not actually be run because the class would cache the first response, and subsequent responses would use that cached response. - Emit helpful warning if allow_url_fopen is not set - Split Github/Zend service calls into their own methods - Early return cached version to reduce nesting - Rename methods for consistency - Consistent usage of self:: instead of static:: as the class is marked 'final' --- library/Zend/Version/Version.php | 174 ++++++++++++++++++++----- tests/ZendTest/Version/VersionTest.php | 102 ++++++++++++++- 2 files changed, 241 insertions(+), 35 deletions(-) diff --git a/library/Zend/Version/Version.php b/library/Zend/Version/Version.php index 9a05cd0d121..5c80515036e 100644 --- a/library/Zend/Version/Version.php +++ b/library/Zend/Version/Version.php @@ -9,6 +9,7 @@ namespace Zend\Version; +use Zend\Http; use Zend\Json\Json; /** @@ -52,6 +53,7 @@ public static function compareVersion($version) { $version = strtolower($version); $version = preg_replace('/(\d)pr(\d?)/', '$1a$2', $version); + return version_compare($version, strtolower(self::VERSION)); } @@ -64,54 +66,160 @@ public static function compareVersion($version) * numbers with version_compare(). * * If $service is set to VERSION_SERVICE_ZEND this will fall back to calling the - * classic style of version retreival. - * + * classic style of version retrieval. * - * @see http://developer.github.com/v3/git/refs/#get-all-references - * @link https://api.github.com/repos/zendframework/zf2/git/refs/tags/release- - * @link http://framework.zend.com/api/zf-version?v=2 - * @param string $service Version Service with which to retrieve the version + * @see http://developer.github.com/v3/git/refs/#get-all-references + * @link https://api.github.com/repos/zendframework/zf2/git/refs/tags/release- + * @link http://framework.zend.com/api/zf-version?v=2 + * @param string $service Version service with which to retrieve the version + * @param Http\Client $httpClient HTTP client with which to retrieve the version * @return string */ - public static function getLatest($service = self::VERSION_SERVICE_ZEND) + public static function getLatest($service = self::VERSION_SERVICE_ZEND, Http\Client $httpClient = null) { - if (null === static::$latestVersion) { - static::$latestVersion = 'not available'; - if ($service == self::VERSION_SERVICE_GITHUB) { - $url = 'https://api.github.com/repos/zendframework/zf2/git/refs/tags/release-'; - - $apiResponse = Json::decode(file_get_contents($url), Json::TYPE_ARRAY); - - // Simplify the API response into a simple array of version numbers - $tags = array_map(function ($tag) { - return substr($tag['ref'], 18); // Reliable because we're filtering on 'refs/tags/release-' - }, $apiResponse); - - // Fetch the latest version number from the array - static::$latestVersion = array_reduce($tags, function ($a, $b) { - return version_compare($a, $b, '>') ? $a : $b; - }); - } elseif ($service == self::VERSION_SERVICE_ZEND) { - $handle = fopen('http://framework.zend.com/api/zf-version?v=2', 'r'); - if (false !== $handle) { - static::$latestVersion = stream_get_contents($handle); - fclose($handle); - } - } + if (null !== self::$latestVersion) { + return self::$latestVersion; + } + + self::$latestVersion = 'not available'; + + if (null === $httpClient && !ini_get('allow_url_fopen')) { + trigger_error( + sprintf( + 'allow_url_fopen is not set, and no Zend\Http\Client ' . + 'was passed. You must either set allow_url_fopen in ' . + 'your PHP configuration or pass a configured ' . + 'Zend\Http\Client as the second argument to %s.', + __METHOD__ + ), + E_USER_WARNING + ); + + return self::$latestVersion; } - return static::$latestVersion; + $response = false; + if ($service === self::VERSION_SERVICE_GITHUB) { + $response = self::getLatestFromGithub($httpClient); + } elseif ($service === self::VERSION_SERVICE_ZEND) { + $response = self::getLatestFromZend($httpClient); + } else { + trigger_error( + sprintf( + 'Unknown version service: %s', + $service + ), + E_USER_WARNING + ); + } + + if ($response) { + self::$latestVersion = $response; + } + + return self::$latestVersion; } /** * Returns true if the running version of Zend Framework is * the latest (or newer??) than the latest tag on GitHub, - * which is returned by static::getLatest(). + * which is returned by self::getLatest(). * * @return bool */ public static function isLatest() { - return static::compareVersion(static::getLatest()) < 1; + return self::compareVersion(self::getLatest()) < 1; + } + + /** + * Get the API response to a call from a configured HTTP client + * + * @param Http\Client $httpClient Configured HTTP client + * @return string|false API response or false on error + */ + protected static function getApiResponse(Http\Client $httpClient) + { + try { + $response = $httpClient->send(); + } catch (Http\Exception\RuntimeException $e) { + return false; + } + + if (!$response->isSuccess()) { + return false; + } + + return $response->getBody(); + } + + /** + * Get the latest version from Github + * + * @param Http\Client $httpClient Configured HTTP client + * @return string|null API response or false on error + */ + protected static function getLatestFromGithub(Http\Client $httpClient = null) + { + $url = 'https://api.github.com/repos/zendframework/zf2/git/refs/tags/release-'; + + if ($httpClient === null) { + $context = stream_context_create( + array( + 'http' => array( + 'user_agent' => sprintf('Zend-Version/%s', self::VERSION), + ), + ) + ); + $apiResponse = file_get_contents($url, false, $context); + } else { + $request = new Http\Request(); + $request->setUri($url); + $httpClient->setRequest($request); + $apiResponse = self::getApiResponse($httpClient); + } + + if (!$apiResponse) { + return false; + } + + $decodedResponse = Json::decode($apiResponse, Json::TYPE_ARRAY); + + // Simplify the API response into a simple array of version numbers + $tags = array_map(function ($tag) { + return substr($tag['ref'], 18); // Reliable because we're + // filtering on 'refs/tags/release-' + }, $decodedResponse); + + // Fetch the latest version number from the array + return array_reduce($tags, function ($a, $b) { + return version_compare($a, $b, '>') ? $a : $b; + }); + } + + /** + * Get the latest version from framework.zend.com + * + * @param Http\Client $httpClient Configured HTTP client + * @return string|null API response or false on error + */ + protected static function getLatestFromZend(Http\Client $httpClient = null) + { + $url = 'http://framework.zend.com/api/zf-version?v=2'; + + if ($httpClient === null) { + $apiResponse = file_get_contents($url); + } else { + $request = new Http\Request(); + $request->setUri($url); + $httpClient->setRequest($request); + $apiResponse = self::getApiResponse($httpClient); + } + + if (!$apiResponse) { + return false; + } + + return $apiResponse; } } diff --git a/tests/ZendTest/Version/VersionTest.php b/tests/ZendTest/Version/VersionTest.php index 1ece36333d7..62a6779d45d 100644 --- a/tests/ZendTest/Version/VersionTest.php +++ b/tests/ZendTest/Version/VersionTest.php @@ -8,6 +8,7 @@ * @package Zend_VersionTest.php */ +use Zend\Http; use Zend\Version\Version; /** @@ -54,7 +55,10 @@ public function testVersionCompare() } /** + * Run in separate process to avoid Version::$latestParameter caching + * * @group ZF-10363 + * @runInSeparateProcess */ public function testFetchLatestVersion() { @@ -64,12 +68,18 @@ public function testFetchLatestVersion() if (!extension_loaded('openssl')) { $this->markTestSkipped('This test requires openssl extension to be enabled in PHP'); } + $actual = Version::getLatest(); $this->assertRegExp('/^[1-2](\.[0-9]+){2}/', $actual); } - public function testFetchLatestZENDVersion() + /** + * Run in separate process to avoid Version::$latestParameter caching + * + * @runInSeparateProcess + */ + public function testFetchLatestGithubVersion() { if (!constant('TESTS_ZEND_VERSION_ONLINE_ENABLED')) { $this->markTestSkipped('Version online tests are not enabled'); @@ -77,8 +87,96 @@ public function testFetchLatestZENDVersion() if (!extension_loaded('openssl')) { $this->markTestSkipped('This test requires openssl extension to be enabled in PHP'); } - $actual = Version::getLatest('ZEND'); + + $actual = Version::getLatest(Version::VERSION_SERVICE_GITHUB); $this->assertRegExp('/^[1-2](\.[0-9]+){2}/', $actual); } + + /** + * Run in separate process to avoid Version::$latestParameter caching + * + * @expectedException PHPUnit_Framework_Error_Warning + * @runInSeparateProcess + */ + public function testFetchLatestVersionWarnsIfAllowUrlFopenIsDisabled() + { + if (!constant('TESTS_ZEND_VERSION_ONLINE_ENABLED')) { + $this->markTestSkipped('Version online tests are not enabled'); + } + if (ini_get('allow_url_fopen')) { + $this->markTestSkipped('Test only works with allow_url_fopen disabled'); + } + + $actual = Version::getLatest(Version::VERSION_SERVICE_ZEND); + } + + /** + * Run in separate process to avoid Version::$latestParameter caching + * + * @expectedException PHPUnit_Framework_Error_Warning + * @runInSeparateProcess + */ + public function testFetchLatestVersionWarnsIfBadServiceIsPassed() + { + if (!constant('TESTS_ZEND_VERSION_ONLINE_ENABLED')) { + $this->markTestSkipped('Version online tests are not enabled'); + } + + $actual = Version::getLatest('bogus service'); + } + + /** + * Run in separate process to avoid Version::$latestParameter caching + * + * @runInSeparateProcess + */ + public function testFetchLatestVersionUsesSuppliedZendHttpClient() + { + if (!constant('TESTS_ZEND_VERSION_ONLINE_ENABLED')) { + $this->markTestSkipped('Version online tests are not enabled'); + } + if (!extension_loaded('openssl')) { + $this->markTestSkipped('This test requires openssl extension to be enabled in PHP'); + } + + $httpClient = new Http\Client( + 'http://example.com', + array( + 'sslverifypeer' => false, + ) + ); + + $actual = Version::getLatest(Version::VERSION_SERVICE_GITHUB, $httpClient); + $this->assertRegExp('/^[1-2](\.[0-9]+){2}/', $actual); + + $lastRequest = $httpClient->getRequest(); + $this->assertContains('github.com', (string) $lastRequest->getUri()); + } + + /** + * Run in separate process to avoid Version::$latestParameter caching + * + * @runInSeparateProcess + */ + public function testFetchLatestVersionDoesNotThrowZendHttpClientException() + { + if (!constant('TESTS_ZEND_VERSION_ONLINE_ENABLED')) { + $this->markTestSkipped('Version online tests are not enabled'); + } + if (!extension_loaded('openssl')) { + $this->markTestSkipped('This test requires openssl extension to be enabled in PHP'); + } + + $httpClient = new Http\Client( + 'http://example.com', + array( + 'sslcapath' => '/dev/null', + 'sslverifypeer' => true, + ) + ); + + $actual = Version::getLatest(Version::VERSION_SERVICE_GITHUB, $httpClient); + $this->assertEquals('not available', $actual); + } } From aaa2e569be06b6f2f954334eb5be0efa843af4b3 Mon Sep 17 00:00:00 2001 From: Gordon Stratton Date: Mon, 10 Jun 2013 23:39:52 +0000 Subject: [PATCH 2/2] Update composer.json to suggest Zend\Http component --- library/Zend/Version/composer.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/Zend/Version/composer.json b/library/Zend/Version/composer.json index 9059e2799dd..1a764fe2cbd 100644 --- a/library/Zend/Version/composer.json +++ b/library/Zend/Version/composer.json @@ -15,6 +15,9 @@ "require": { "php": ">=5.3.3" }, + "suggest": { + "zendframework/zend-http": "Allows use of Zend\\Http\\Client to check version information" + }, "extra": { "branch-alias": { "dev-master": "2.2-dev",